@driftengine/assets 4.6.1 → 4.7.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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/blend.d.ts +25 -0
  3. package/dist/blend.js +43 -0
  4. package/dist/blendAnimation.d.ts +45 -0
  5. package/dist/blendAnimation.js +294 -0
  6. package/dist/blendData.d.ts +114 -0
  7. package/dist/blendData.js +366 -0
  8. package/dist/blendFile.d.ts +69 -0
  9. package/dist/blendFile.js +182 -0
  10. package/dist/blendGeometry.d.ts +45 -0
  11. package/dist/blendGeometry.js +210 -0
  12. package/dist/blendGltf.d.ts +65 -0
  13. package/dist/blendGltf.js +545 -0
  14. package/dist/blendMaterial.d.ts +68 -0
  15. package/dist/blendMaterial.js +545 -0
  16. package/dist/blendMatrix.d.ts +28 -0
  17. package/dist/blendMatrix.js +245 -0
  18. package/dist/blendMesh.d.ts +70 -0
  19. package/dist/blendMesh.js +409 -0
  20. package/dist/blendModifiers.d.ts +25 -0
  21. package/dist/blendModifiers.js +83 -0
  22. package/dist/blendNormals.d.ts +44 -0
  23. package/dist/blendNormals.js +458 -0
  24. package/dist/blendPacked.d.ts +35 -0
  25. package/dist/blendPacked.js +82 -0
  26. package/dist/blendScene.d.ts +50 -0
  27. package/dist/blendScene.js +280 -0
  28. package/dist/gltf.js +8 -2
  29. package/dist/index.d.ts +21 -0
  30. package/dist/index.js +17 -0
  31. package/dist/readModel.d.ts +13 -1
  32. package/dist/readModel.js +43 -13
  33. package/package.json +4 -4
  34. package/src/blend.ts +57 -0
  35. package/src/blendAnimation.ts +328 -0
  36. package/src/blendData.ts +375 -0
  37. package/src/blendFile.ts +233 -0
  38. package/src/blendGeometry.ts +263 -0
  39. package/src/blendGltf.ts +621 -0
  40. package/src/blendMaterial.ts +676 -0
  41. package/src/blendMatrix.ts +257 -0
  42. package/src/blendMesh.ts +496 -0
  43. package/src/blendModifiers.ts +96 -0
  44. package/src/blendNormals.ts +481 -0
  45. package/src/blendPacked.ts +93 -0
  46. package/src/blendScene.ts +307 -0
  47. package/src/gltf.ts +14 -2
  48. package/src/index.ts +41 -0
  49. package/src/readModel.ts +58 -13
@@ -0,0 +1,481 @@
1
+ /**
2
+ * The normal Blender shades each face corner with, computed from what a `.blend` stores.
3
+ *
4
+ * **A `.blend` stores no shading normals unless somebody customised them**, so a reader has to
5
+ * derive the ones Blender draws, and "a smooth normal" is not specific enough to agree with it.
6
+ * Blender's rule, which this follows step for step:
7
+ *
8
+ * - Every face flat, or every edge sharp: each corner takes its face's normal.
9
+ * - Every face smooth and no edge sharp: each corner takes its vertex's normal, the sum of the
10
+ * normals of every face at the vertex weighted by the angle each face makes there.
11
+ * - Otherwise the corners around a vertex split into *fans* at every sharp edge — an edge tagged
12
+ * sharp, an edge beside a flat face, an edge with more or fewer than two faces, and an edge whose
13
+ * faces disagree about winding — and each fan takes the angle-weighted sum over its own faces.
14
+ *
15
+ * **Custom normals are stored relative to those fans**, two shorts per corner: an angle away from
16
+ * the fan's normal and an angle around it from the fan's first edge. Decoding them needs the fans
17
+ * walked in exactly Blender's order, which is why the walk below starts each fan where Blender
18
+ * starts it and visits faces in the direction Blender turns. A vector normal stored as an
19
+ * attribute — 4.5 on — needs none of this and is used as it stands.
20
+ *
21
+ * What it gives up: nothing it knows of. Its arithmetic is in doubles where Blender's is in
22
+ * floats, which is a difference of about 1e-4 in a normal and is what the fixtures allow.
23
+ */
24
+
25
+ import type { BlendMeshData } from './blendMesh.ts';
26
+ import { FACE, POINT } from './blendMesh.ts';
27
+
28
+ const EMPTY = -1;
29
+ const UNSET = -2;
30
+ const INVALID = -3;
31
+ /** Blender's limit beyond which two directions are too close to define a frame between them. */
32
+ const TRIGO_THRESHOLD = 1 - 1e-4;
33
+ const TAU = Math.PI * 2;
34
+
35
+ type Vec = [number, number, number];
36
+
37
+ const acos = (x: number): number => Math.acos(Math.max(-1, Math.min(1, x)));
38
+ const f32 = Math.fround;
39
+ /** Blender's approximate arc cosine for corner angles, in its single precision. */
40
+ function cornerAngle(x: number): number {
41
+ const f = f32(Math.abs(x));
42
+ const m = f < 1 ? f32(1 - f32(1 - f)) : 1;
43
+ const a = f32(
44
+ Math.sqrt(f32(1 - m)) *
45
+ f32(1.5707963267 + m * f32(-0.213300989 + m * f32(0.077980478 + m * -0.02164095))),
46
+ );
47
+ return x < 0 ? f32(Math.PI - a) : a;
48
+ }
49
+ const dot = (a: Vec, b: Vec): number => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
50
+ function normalise(v: Vec): number {
51
+ const length = Math.hypot(v[0], v[1], v[2]);
52
+ if (length > 1e-35) {
53
+ v[0] /= length;
54
+ v[1] /= length;
55
+ v[2] /= length;
56
+ } else {
57
+ v[0] = v[1] = v[2] = 0;
58
+ }
59
+ return length;
60
+ }
61
+
62
+ /**
63
+ * Each face's normal, by the rule Blender uses for its size: two edges of a triangle, the two
64
+ * diagonals of a quad, and Newell's method beyond that.
65
+ *
66
+ * **Not Newell's method throughout**, which is what this was until a real file disagreed: a quad
67
+ * with two coincident corners has no area, so Newell gives it no normal, while its diagonals still
68
+ * cross. And where even they cancel — a quad whose corners coincide in pairs — Blender calls the
69
+ * face's normal straight up rather than zero, and averages that into its smooth neighbours. Both
70
+ * were found on a 2.79 wheel, where one such face moved a vertex normal 0.68 from Blender's.
71
+ */
72
+ export function faceNormals(mesh: BlendMeshData): Float64Array {
73
+ const { positions: p, faceOffsets, cornerVerts } = mesh;
74
+ const faces = faceOffsets.length - 1;
75
+ const out = new Float64Array(faces * 3);
76
+ const at = (corner: number, axis: number): number =>
77
+ p[(cornerVerts[corner] as number) * 3 + axis] as number;
78
+ for (let f = 0; f < faces; f++) {
79
+ const start = faceOffsets[f] as number;
80
+ const end = faceOffsets[f + 1] as number;
81
+ const n: Vec = [0, 0, 0];
82
+ if (end - start === 3 || end - start === 4) {
83
+ /* Triangle: (v0 - v1) x (v1 - v2). Quad: (v0 - v2) x (v1 - v3). */
84
+ const quad = end - start === 4;
85
+ const a = [0, 1, 2].map((k) => at(start, k) - at(start + (quad ? 2 : 1), k));
86
+ const b = [0, 1, 2].map((k) => at(start + 1, k) - at(start + (quad ? 3 : 2), k));
87
+ n[0] = (a[1] as number) * (b[2] as number) - (a[2] as number) * (b[1] as number);
88
+ n[1] = (a[2] as number) * (b[0] as number) - (a[0] as number) * (b[2] as number);
89
+ n[2] = (a[0] as number) * (b[1] as number) - (a[1] as number) * (b[0] as number);
90
+ } else {
91
+ let prev = (cornerVerts[end - 1] as number) * 3;
92
+ for (let c = start; c < end; c++) {
93
+ const curr = (cornerVerts[c] as number) * 3;
94
+ const px = p[prev] as number,
95
+ py = p[prev + 1] as number,
96
+ pz = p[prev + 2] as number;
97
+ const cx = p[curr] as number,
98
+ cy = p[curr + 1] as number,
99
+ cz = p[curr + 2] as number;
100
+ n[0] += (py - cy) * (pz + cz);
101
+ n[1] += (pz - cz) * (px + cx);
102
+ n[2] += (px - cx) * (py + cy);
103
+ prev = curr;
104
+ }
105
+ }
106
+ /* Blender's convention for a face with no area: it faces up. Its neighbours average it in. */
107
+ if (normalise(n) === 0) n[2] = 1;
108
+ out.set(n, f * 3);
109
+ }
110
+ return out;
111
+ }
112
+
113
+ function direction(mesh: BlendMeshData, from: number, to: number): Vec {
114
+ const p = mesh.positions;
115
+ const v: Vec = [
116
+ (p[to * 3] as number) - (p[from * 3] as number),
117
+ (p[to * 3 + 1] as number) - (p[from * 3 + 1] as number),
118
+ (p[to * 3 + 2] as number) - (p[from * 3 + 2] as number),
119
+ ];
120
+ normalise(v);
121
+ return v;
122
+ }
123
+
124
+ /** The angle-weighted vertex normals, what an all-smooth mesh shades with. */
125
+ export function vertexNormals(mesh: BlendMeshData, face: Float64Array): Float64Array {
126
+ const { faceOffsets, cornerVerts, positions } = mesh;
127
+ const verts = positions.length / 3;
128
+ const out = new Float64Array(verts * 3);
129
+ for (let f = 0; f < faceOffsets.length - 1; f++) {
130
+ const start = faceOffsets[f] as number;
131
+ const end = faceOffsets[f + 1] as number;
132
+ for (let c = start; c < end; c++) {
133
+ const v = cornerVerts[c] as number;
134
+ const prev = cornerVerts[c === start ? end - 1 : c - 1] as number;
135
+ const next = cornerVerts[c + 1 === end ? start : c + 1] as number;
136
+ const angle = cornerAngle(dot(direction(mesh, v, prev), direction(mesh, v, next)));
137
+ for (let k = 0; k < 3; k++)
138
+ out[v * 3 + k] = (out[v * 3 + k] as number) + (face[f * 3 + k] as number) * angle;
139
+ }
140
+ }
141
+ for (let v = 0; v < verts; v++) {
142
+ const n: Vec = [out[v * 3] as number, out[v * 3 + 1] as number, out[v * 3 + 2] as number];
143
+ /* Blender's convention for a vertex with no usable faces: point the normal along its position. */
144
+ if (normalise(n) === 0) {
145
+ n[0] = positions[v * 3] as number;
146
+ n[1] = positions[v * 3 + 1] as number;
147
+ n[2] = positions[v * 3 + 2] as number;
148
+ normalise(n);
149
+ }
150
+ out.set(n, v * 3);
151
+ }
152
+ return out;
153
+ }
154
+
155
+ /** Every edge that splits a fan: tagged, beside a flat face, not two-faced, or flipped. */
156
+ function edgeLoops(mesh: BlendMeshData, sharpEdge: Uint8Array | null): Int32Array {
157
+ const { faceOffsets, cornerVerts, cornerEdges, sharpFace } = mesh;
158
+ const edges = mesh.edgeVerts.length / 2;
159
+ const e2l = new Int32Array(edges * 2).fill(EMPTY);
160
+ for (let f = 0; f < faceOffsets.length - 1; f++) {
161
+ const flat = sharpFace !== null && sharpFace[f] === 1;
162
+ for (let c = faceOffsets[f] as number; c < (faceOffsets[f + 1] as number); c++) {
163
+ const e = cornerEdges[c] as number;
164
+ if (e2l[e * 2] === EMPTY) {
165
+ e2l[e * 2] = c;
166
+ e2l[e * 2 + 1] = flat ? INVALID : UNSET;
167
+ } else if (e2l[e * 2 + 1] === UNSET) {
168
+ const first = e2l[e * 2] as number;
169
+ if (
170
+ flat ||
171
+ (sharpEdge !== null && sharpEdge[e] === 1) ||
172
+ cornerVerts[c] === cornerVerts[first]
173
+ ) {
174
+ e2l[e * 2 + 1] = INVALID;
175
+ } else e2l[e * 2 + 1] = c;
176
+ } else if (e2l[e * 2 + 1] !== INVALID) {
177
+ e2l[e * 2 + 1] = INVALID;
178
+ }
179
+ }
180
+ }
181
+ return e2l;
182
+ }
183
+
184
+ const isSharp = (e2l: Int32Array, edge: number): boolean => {
185
+ const second = e2l[edge * 2 + 1] as number;
186
+ return second === UNSET || second === INVALID;
187
+ };
188
+
189
+ /** The frame a fan's custom normals are measured in. */
190
+ interface Space {
191
+ lnor: Vec;
192
+ ref: Vec;
193
+ ortho: Vec;
194
+ alpha: number;
195
+ beta: number;
196
+ }
197
+
198
+ function defineSpace(lnor: Vec, ref: Vec, other: Vec, edges: readonly Vec[]): Space {
199
+ const space: Space = { lnor: [...lnor], ref: [0, 0, 0], ortho: [0, 0, 0], alpha: 0, beta: 0 };
200
+ const dRef = dot(ref, lnor);
201
+ const dOther = dot(other, lnor);
202
+ if (Math.abs(dRef) >= TRIGO_THRESHOLD || Math.abs(dOther) >= TRIGO_THRESHOLD) return space;
203
+ if (edges.length > 0) {
204
+ let alpha = 0;
205
+ for (const e of edges) alpha += acos(dot(e, lnor));
206
+ space.alpha = alpha / edges.length;
207
+ } else {
208
+ space.alpha = (acos(dRef) + acos(dOther)) / 2;
209
+ }
210
+ const r: Vec = [ref[0] - lnor[0] * dRef, ref[1] - lnor[1] * dRef, ref[2] - lnor[2] * dRef];
211
+ normalise(r);
212
+ space.ref = r;
213
+ const o: Vec = [
214
+ lnor[1] * r[2] - lnor[2] * r[1],
215
+ lnor[2] * r[0] - lnor[0] * r[2],
216
+ lnor[0] * r[1] - lnor[1] * r[0],
217
+ ];
218
+ normalise(o);
219
+ space.ortho = o;
220
+ const t: Vec = [
221
+ other[0] - lnor[0] * dOther,
222
+ other[1] - lnor[1] * dOther,
223
+ other[2] - lnor[2] * dOther,
224
+ ];
225
+ normalise(t);
226
+ const d = dot(r, t);
227
+ if (d < TRIGO_THRESHOLD) {
228
+ const beta = acos(d);
229
+ space.beta = dot(o, t) < 0 ? TAU - beta : beta;
230
+ } else space.beta = TAU;
231
+ return space;
232
+ }
233
+
234
+ function decode(space: Space, a: number, b: number): Vec {
235
+ if (a === 0 || space.alpha === 0 || space.beta === 0) return [...space.lnor];
236
+ const alphaFac = a / 32767;
237
+ const alpha = (alphaFac > 0 ? space.alpha : TAU - space.alpha) * alphaFac;
238
+ const out: Vec = [
239
+ space.lnor[0] * Math.cos(alpha),
240
+ space.lnor[1] * Math.cos(alpha),
241
+ space.lnor[2] * Math.cos(alpha),
242
+ ];
243
+ const sinAlpha = Math.sin(alpha);
244
+ if (b === 0) {
245
+ for (let k = 0; k < 3; k++) out[k] = (out[k] as number) + space.ref[k as 0 | 1 | 2] * sinAlpha;
246
+ return out;
247
+ }
248
+ const betaFac = b / 32767;
249
+ const beta = (betaFac > 0 ? space.beta : TAU - space.beta) * betaFac;
250
+ for (let k = 0; k < 3; k++) {
251
+ const i = k as 0 | 1 | 2;
252
+ out[i] += space.ref[i] * sinAlpha * Math.cos(beta) + space.ortho[i] * sinAlpha * Math.sin(beta);
253
+ }
254
+ return out;
255
+ }
256
+
257
+ /**
258
+ * The normal every corner is shaded with, three floats per corner, in the mesh's own space.
259
+ *
260
+ * `splitAngle` also tags as sharp every edge whose faces meet at more than it: a pre-4.1 file's
261
+ * auto smooth by default, or the angle of a Smooth by Angle modifier, which does the same thing.
262
+ */
263
+ export function cornerNormals(
264
+ mesh: BlendMeshData,
265
+ splitAngle: number | null = mesh.autoSmooth,
266
+ ): Float32Array {
267
+ const { faceOffsets, cornerVerts, cornerEdges, edgeVerts, sharpFace } = mesh;
268
+ const corners = cornerVerts.length;
269
+ const faces = faceOffsets.length - 1;
270
+ const out = new Float32Array(corners * 3);
271
+ const face = faceNormals(mesh);
272
+ const faceOf = new Int32Array(corners);
273
+ for (let f = 0; f < faces; f++)
274
+ faceOf.fill(f, faceOffsets[f] as number, faceOffsets[f + 1] as number);
275
+
276
+ if (mesh.freeNormals !== null && mesh.packedNormals === null) {
277
+ const { domain, normals } = mesh.freeNormals;
278
+ for (let c = 0; c < corners; c++) {
279
+ const i =
280
+ domain === POINT ? (cornerVerts[c] as number) : domain === FACE ? (faceOf[c] as number) : c;
281
+ const n: Vec = [
282
+ normals[i * 3] as number,
283
+ normals[i * 3 + 1] as number,
284
+ normals[i * 3 + 2] as number,
285
+ ];
286
+ normalise(n);
287
+ out.set(n, c * 3);
288
+ }
289
+ return out;
290
+ }
291
+
292
+ let sharpEdge = mesh.sharpEdge;
293
+ if (splitAngle !== null) sharpEdge = byAngle(mesh, face, splitAngle);
294
+ const allFlat = faces > 0 && sharpFace !== null && sharpFace.every((s) => s === 1);
295
+ const allSharp = sharpEdge !== null && sharpEdge.length > 0 && sharpEdge.every((s) => s === 1);
296
+ const anyFlat = sharpFace !== null && sharpFace.some((s) => s === 1);
297
+ const anySharp = sharpEdge !== null && sharpEdge.some((s) => s === 1);
298
+
299
+ if (mesh.packedNormals === null && (allFlat || allSharp)) {
300
+ for (let c = 0; c < corners; c++)
301
+ out.set(face.subarray((faceOf[c] as number) * 3, (faceOf[c] as number) * 3 + 3), c * 3);
302
+ return out;
303
+ }
304
+ const vertex = vertexNormals(mesh, face);
305
+ if (mesh.packedNormals === null && !anyFlat && !anySharp) {
306
+ for (let c = 0; c < corners; c++) {
307
+ const v = cornerVerts[c] as number;
308
+ out.set(vertex.subarray(v * 3, v * 3 + 3), c * 3);
309
+ }
310
+ return out;
311
+ }
312
+
313
+ const e2l = edgeLoops(mesh, sharpEdge);
314
+ const packed = mesh.packedNormals;
315
+ const other = (edge: number, pivot: number): number =>
316
+ (edgeVerts[edge * 2] as number) === pivot
317
+ ? (edgeVerts[edge * 2 + 1] as number)
318
+ : (edgeVerts[edge * 2] as number);
319
+ const prevCorner = (c: number): number => {
320
+ const f = faceOf[c] as number;
321
+ return c === faceOffsets[f] ? (faceOffsets[f + 1] as number) - 1 : c - 1;
322
+ };
323
+ const nextCorner = (c: number): number => {
324
+ const f = faceOf[c] as number;
325
+ return c + 1 === faceOffsets[f + 1] ? (faceOffsets[f] as number) : c + 1;
326
+ };
327
+
328
+ /* Prefilled as Blender prefills: the vertex normal where smooth, the face normal where flat. */
329
+ for (let c = 0; c < corners; c++) {
330
+ const f = faceOf[c] as number;
331
+ if (sharpFace !== null && sharpFace[f] === 1) out.set(face.subarray(f * 3, f * 3 + 3), c * 3);
332
+ else {
333
+ const v = cornerVerts[c] as number;
334
+ out.set(vertex.subarray(v * 3, v * 3 + 3), c * 3);
335
+ }
336
+ }
337
+
338
+ /** Cross the edge `curr` names into the next face around `pivot`: Blender's fan step. */
339
+ const step = (curr: number, pivot: number): { curr: number; vert: number } => {
340
+ const edge = cornerEdges[curr] as number;
341
+ const a = e2l[edge * 2] as number;
342
+ const b = e2l[edge * 2 + 1] as number;
343
+ const fromVert = cornerVerts[curr] as number;
344
+ const next = a === curr ? b : a;
345
+ const nextVert = cornerVerts[next] as number;
346
+ if (
347
+ (fromVert === nextVert && fromVert === pivot) ||
348
+ (fromVert !== nextVert && fromVert !== pivot)
349
+ ) {
350
+ return { curr: prevCorner(next), vert: next };
351
+ }
352
+ const forward = nextCorner(next);
353
+ return { curr: forward, vert: forward };
354
+ };
355
+
356
+ const skip = new Uint8Array(corners);
357
+ const cyclic = (start: number, prev: number): boolean => {
358
+ const pivot = cornerVerts[start] as number;
359
+ if (isSharp(e2l, cornerEdges[prev] as number)) return false;
360
+ let curr = prev;
361
+ let vert = start;
362
+ skip[vert] = 1;
363
+ for (;;) {
364
+ ({ curr, vert } = step(curr, pivot));
365
+ if (isSharp(e2l, cornerEdges[curr] as number)) return false;
366
+ if (skip[vert] === 1) return vert === start;
367
+ skip[vert] = 1;
368
+ }
369
+ };
370
+
371
+ const single = (c: number): void => {
372
+ const f = faceOf[c] as number;
373
+ const n: Vec = [face[f * 3] as number, face[f * 3 + 1] as number, face[f * 3 + 2] as number];
374
+ if (packed !== null) {
375
+ const v = cornerVerts[c] as number;
376
+ const space = defineSpace(
377
+ n,
378
+ direction(mesh, v, cornerVerts[nextCorner(c)] as number),
379
+ direction(mesh, v, cornerVerts[prevCorner(c)] as number),
380
+ [],
381
+ );
382
+ out.set(decode(space, packed[c * 2] as number, packed[c * 2 + 1] as number), c * 3);
383
+ } else out.set(n, c * 3);
384
+ };
385
+
386
+ const fan = (start: number, prev: number): void => {
387
+ const pivot = cornerVerts[start] as number;
388
+ const origin = cornerEdges[start] as number;
389
+ const vecOrigin = direction(mesh, pivot, other(origin, pivot));
390
+ let vecPrev: Vec = vecOrigin;
391
+ let vecCurr: Vec = vecOrigin;
392
+ const edges: Vec[] = [vecOrigin];
393
+ const members: number[] = [];
394
+ const lnor: Vec = [0, 0, 0];
395
+ let curr = prev;
396
+ let vert = start;
397
+ for (let guard = 0; guard <= corners; guard++) {
398
+ const edge = cornerEdges[curr] as number;
399
+ vecCurr = direction(mesh, pivot, other(edge, pivot));
400
+ const angle = cornerAngle(dot(vecCurr, vecPrev));
401
+ const f = faceOf[curr] as number;
402
+ for (let k = 0; k < 3; k++) lnor[k as 0 | 1 | 2] += (face[f * 3 + k] as number) * angle;
403
+ members.push(vert);
404
+ if (edge !== origin) edges.push(vecCurr);
405
+ if (isSharp(e2l, edge) || edge === origin) break;
406
+ vecPrev = vecCurr;
407
+ ({ curr, vert } = step(curr, pivot));
408
+ }
409
+ let length = normalise(lnor);
410
+ if (packed !== null) {
411
+ if (length === 0) {
412
+ const v = cornerVerts[vert] as number;
413
+ lnor[0] = vertex[v * 3] as number;
414
+ lnor[1] = vertex[v * 3 + 1] as number;
415
+ lnor[2] = vertex[v * 3 + 2] as number;
416
+ length = 1;
417
+ }
418
+ const space = defineSpace(lnor, vecOrigin, vecCurr, edges);
419
+ const first = members[0] as number;
420
+ let a = packed[first * 2] as number;
421
+ let b = packed[first * 2 + 1] as number;
422
+ if (members.some((m) => packed[m * 2] !== a || packed[m * 2 + 1] !== b)) {
423
+ let sa = 0;
424
+ let sb = 0;
425
+ for (const m of members) {
426
+ sa += packed[m * 2] as number;
427
+ sb += packed[m * 2 + 1] as number;
428
+ }
429
+ a = Math.trunc(sa / members.length);
430
+ b = Math.trunc(sb / members.length);
431
+ }
432
+ const custom = decode(space, a, b);
433
+ lnor[0] = custom[0];
434
+ lnor[1] = custom[1];
435
+ lnor[2] = custom[2];
436
+ }
437
+ if (length !== 0) for (const m of members) out.set(lnor, m * 3);
438
+ };
439
+
440
+ for (let f = 0; f < faces; f++) {
441
+ for (let c = faceOffsets[f] as number; c < (faceOffsets[f + 1] as number); c++) {
442
+ const prev = prevCorner(c);
443
+ const currSharp = isSharp(e2l, cornerEdges[c] as number);
444
+ const prevSharp = isSharp(e2l, cornerEdges[prev] as number);
445
+ if (currSharp && prevSharp) single(c);
446
+ else if (currSharp || (!prevSharp && skip[c] === 0 && cyclic(c, prev))) fan(c, prev);
447
+ }
448
+ }
449
+ return out;
450
+ }
451
+
452
+ /** What Blender's smooth-by-angle does to a pre-4.1 file with auto smooth on: tag by angle too. */
453
+ function byAngle(mesh: BlendMeshData, face: Float64Array, angle: number): Uint8Array {
454
+ const edges = mesh.edgeVerts.length / 2;
455
+ const out = new Uint8Array(edges);
456
+ if (mesh.sharpEdge !== null) out.set(mesh.sharpEdge);
457
+ const first = new Int32Array(edges).fill(-1);
458
+ const count = new Int32Array(edges);
459
+ const second = new Int32Array(edges).fill(-1);
460
+ const { faceOffsets, cornerEdges } = mesh;
461
+ for (let f = 0; f < faceOffsets.length - 1; f++) {
462
+ for (let c = faceOffsets[f] as number; c < (faceOffsets[f + 1] as number); c++) {
463
+ const e = cornerEdges[c] as number;
464
+ if (count[e] === 0) first[e] = f;
465
+ else if (count[e] === 1) second[e] = f;
466
+ count[e] = (count[e] as number) + 1;
467
+ }
468
+ }
469
+ const cos = Math.cos(angle);
470
+ for (let e = 0; e < edges; e++) {
471
+ if (count[e] !== 2) continue;
472
+ const a = first[e] as number;
473
+ const b = second[e] as number;
474
+ const d =
475
+ (face[a * 3] as number) * (face[b * 3] as number) +
476
+ (face[a * 3 + 1] as number) * (face[b * 3 + 1] as number) +
477
+ (face[a * 3 + 2] as number) * (face[b * 3 + 2] as number);
478
+ if (d < cos) out[e] = 1;
479
+ }
480
+ return out;
481
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Undoing the compression a `.blend` was saved with, through a decompressor the host supplies.
3
+ *
4
+ * **Blender has saved compressed files two ways.** Before 3.0 the whole file was one gzip stream.
5
+ * From 3.0 it is zstd in the *seekable* layout: the file cut into independent frames, and a table
6
+ * at the end in a skippable frame saying how long each one is. This module reads that table and
7
+ * hands each frame to the host's decoder, because a decoder asked for the whole file may stop at
8
+ * the end of the first frame and say nothing: Node 22's `zstdDecompressSync` returned 67 KB of a
9
+ * three-frame, 330 KB fixture, which then read as a file that ends before its `ENDB`.
10
+ *
11
+ * **The decoder is the host's, not this module's.** Node has both; a browser has gzip in
12
+ * `DecompressionStream` and, depending on the browser, no zstd at all. Shipping a zstd decoder
13
+ * here would be owning a correctness risk the platform already carries, which is the argument
14
+ * `fbxInflate.ts` makes for deflate. A host without one is told the name of what to pass.
15
+ *
16
+ * What it gives up: a zstd file with no seek table — one no Blender writes — is handed to the
17
+ * decoder whole, and a decoder that stops after one frame then truncates it. The truncation is
18
+ * caught downstream, by the missing `ENDB`, rather than here.
19
+ */
20
+
21
+ import { DrftError } from '@driftengine/drft';
22
+ import { blendCompression } from './blendFile.ts';
23
+
24
+ /** Decompress one gzip stream or one zstd frame. Supplied by the host. */
25
+ export type BlendDecompress = (
26
+ compressed: Uint8Array,
27
+ codec: 'gzip' | 'zstd',
28
+ ) => Uint8Array | Promise<Uint8Array>;
29
+
30
+ const SEEKABLE_MAGIC = 0x8f92eab1;
31
+ const SKIPPABLE_MAGIC = 0x184d2a5e;
32
+
33
+ /** Each frame's compressed and decompressed length, from the seek table, or null where there is none. */
34
+ export function zstdFrames(bytes: Uint8Array): { packed: number; plain: number }[] | null {
35
+ if (bytes.length < 17) return null;
36
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
37
+ const footer = bytes.length - 9;
38
+ if (view.getUint32(footer + 5, true) !== SEEKABLE_MAGIC) return null;
39
+ const frames = view.getUint32(footer, true);
40
+ const descriptor = bytes[footer + 4] ?? 0;
41
+ const entry = descriptor & 0x80 ? 12 : 8;
42
+ const table = footer - frames * entry;
43
+ const header = table - 8;
44
+ if (header < 0 || view.getUint32(header, true) !== SKIPPABLE_MAGIC) return null;
45
+ const sizes: { packed: number; plain: number }[] = [];
46
+ let total = 0;
47
+ for (let i = 0; i < frames; i++) {
48
+ const packed = view.getUint32(table + i * entry, true);
49
+ sizes.push({ packed, plain: view.getUint32(table + i * entry + 4, true) });
50
+ total += packed;
51
+ }
52
+ return total === header ? sizes : null;
53
+ }
54
+
55
+ /**
56
+ * The plain bytes of a `.blend`, decompressing first where the file says it is compressed.
57
+ *
58
+ * **Allocated once, from the seek table.** The table states every frame's decompressed length, so
59
+ * the output is sized before the first frame is decoded and each frame is copied into place and
60
+ * dropped. Collecting the frames and joining them afterwards held the file twice at its peak, which
61
+ * is 14 GB for a 7 GB city and was the difference between reading it and not.
62
+ */
63
+ export async function unpackBlend(
64
+ bytes: Uint8Array,
65
+ decompress: BlendDecompress | undefined,
66
+ ): Promise<Uint8Array> {
67
+ const codec = blendCompression(bytes);
68
+ if (codec === null) return bytes;
69
+ if (decompress === undefined) {
70
+ throw new DrftError(
71
+ `.blend saved with ${codec} compression needs a "decompress" to be read. Bake it with ` +
72
+ '`npm run bake`, which supplies one, or pass decompress to readModel.',
73
+ );
74
+ }
75
+ if (codec === 'gzip') return decompress(bytes, 'gzip');
76
+ const frames = zstdFrames(bytes);
77
+ if (frames === null) return decompress(bytes, 'zstd');
78
+ const out = new Uint8Array(frames.reduce((n, frame) => n + frame.plain, 0));
79
+ let from = 0;
80
+ let to = 0;
81
+ for (const frame of frames) {
82
+ const part = await decompress(bytes.subarray(from, from + frame.packed), 'zstd');
83
+ if (part.length !== frame.plain) {
84
+ throw new DrftError(
85
+ `blend: a zstd frame decoded to ${part.length} bytes where the seek table says ${frame.plain}`,
86
+ );
87
+ }
88
+ out.set(part, to);
89
+ from += frame.packed;
90
+ to += part.length;
91
+ }
92
+ return out;
93
+ }