@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,233 @@
1
+ /**
2
+ * A `.blend` file's container: its header, its blocks, and the catalogue of structures the file
3
+ * carries to describe its own layout.
4
+ *
5
+ * **The catalogue is what makes this readable at all.** Every `.blend` ends with a `DNA1` block
6
+ * listing each C struct the writing Blender knew — its fields, their types, their sizes — so a
7
+ * reader learns the layout *from the file* rather than hard-coding one Blender release. A mesh
8
+ * written by 2.79 and one written by 5.2 differ in nearly every struct, and both are read by the
9
+ * same code because neither is assumed.
10
+ *
11
+ * **Three container shapes are read, and anything else is refused by name.** The twelve-byte
12
+ * header every release before 5.0 wrote, with four- or eight-byte pointers and either byte order,
13
+ * and the seventeen-byte header 5.0 introduced, whose block headers carry 64-bit lengths. A file
14
+ * compressed with gzip (before 3.0) or zstd (3.0 on) is reported as such, and `readBlendFile`
15
+ * takes the decompressed bytes: decompressing is the host's capability, not this module's.
16
+ *
17
+ * What this gives up: a block whose pointer is not the start of a block is found by search, which
18
+ * is a binary search over every block. That is the cost of not assuming how Blender allocates.
19
+ */
20
+
21
+ import { DrftError } from '@driftengine/drft';
22
+
23
+ /** What the first bytes say, before any block is read. */
24
+ export interface BlendHeader {
25
+ readonly pointerSize: 4 | 8;
26
+ readonly littleEndian: boolean;
27
+ /** The writing Blender's version, as the header spells it: 279, 405, 500. */
28
+ readonly version: number;
29
+ /** Bytes of header before the first block. */
30
+ readonly headerSize: number;
31
+ /** Bytes in each block header: 20, 24, or 32 for the 64-bit form 5.0 writes. */
32
+ readonly blockHeaderSize: number;
33
+ }
34
+
35
+ /** One block: a run of `count` structs of catalogue type `sdna`, at `offset` in the file. */
36
+ export interface BlendBlock {
37
+ readonly code: string;
38
+ readonly sdna: number;
39
+ /** The address the struct had in the writing process, which every pointer to it names. */
40
+ readonly address: bigint;
41
+ readonly length: number;
42
+ readonly count: number;
43
+ readonly offset: number;
44
+ }
45
+
46
+ /** One field of a catalogued struct, with its offset already summed. */
47
+ export interface DnaField {
48
+ /** The bare name: `*next` and `co[3]` are `next` and `co`. */
49
+ readonly name: string;
50
+ readonly type: string;
51
+ readonly offset: number;
52
+ readonly size: number;
53
+ readonly pointer: boolean;
54
+ /** Array dimensions, outermost first; empty for a scalar. */
55
+ readonly dims: readonly number[];
56
+ }
57
+
58
+ export interface DnaStruct {
59
+ readonly index: number;
60
+ readonly type: string;
61
+ readonly size: number;
62
+ readonly fields: ReadonlyMap<string, DnaField>;
63
+ }
64
+
65
+ /** Which compression, if any, a file's first bytes declare. */
66
+ export type BlendCompression = 'gzip' | 'zstd' | null;
67
+
68
+ /** The compression a `.blend` was saved with, by its magic, or null for a plain file. */
69
+ export function blendCompression(bytes: Uint8Array): BlendCompression {
70
+ if (bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b) return 'gzip';
71
+ if (bytes.length >= 4 && bytes[0] === 0x28 && bytes[1] === 0xb5 && bytes[2] === 0x2f) {
72
+ return bytes[3] === 0xfd ? 'zstd' : null;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ /** Whether these bytes are an uncompressed `.blend`. */
78
+ export function isBlend(bytes: Uint8Array): boolean {
79
+ const magic = 'BLENDER';
80
+ if (bytes.length < 12) return false;
81
+ for (let i = 0; i < magic.length; i++) if (bytes[i] !== magic.charCodeAt(i)) return false;
82
+ return true;
83
+ }
84
+
85
+ const ascii = (bytes: Uint8Array, at: number, length: number): string =>
86
+ String.fromCharCode(...bytes.subarray(at, at + length));
87
+
88
+ /** Parse the header, refusing any shape this module has not been taught. */
89
+ export function readBlendHeader(bytes: Uint8Array): BlendHeader {
90
+ if (!isBlend(bytes)) {
91
+ const packed = blendCompression(bytes);
92
+ throw new DrftError(
93
+ packed === null
94
+ ? 'blend: not a .blend file (no BLENDER magic)'
95
+ : `blend: this file is ${packed}-compressed and must be decompressed before it is read`,
96
+ );
97
+ }
98
+ const seventh = String.fromCharCode(bytes[7] ?? 0);
99
+ if (seventh === '-' || seventh === '_') {
100
+ const endian = String.fromCharCode(bytes[8] ?? 0);
101
+ if (endian !== 'v' && endian !== 'V') throw new DrftError(`blend: byte order "${endian}"`);
102
+ const pointerSize = seventh === '-' ? 8 : 4;
103
+ return {
104
+ pointerSize,
105
+ littleEndian: endian === 'v',
106
+ version: Number(ascii(bytes, 9, 3)),
107
+ headerSize: 12,
108
+ blockHeaderSize: pointerSize === 8 ? 24 : 20,
109
+ };
110
+ }
111
+ /* 5.0 on: `BLENDER` + header size + `-` + file format version + byte order + four digits. */
112
+ const headerSize = Number(ascii(bytes, 7, 2));
113
+ const format = Number(ascii(bytes, 10, 2));
114
+ const endian = String.fromCharCode(bytes[12] ?? 0);
115
+ if (
116
+ headerSize !== 17 ||
117
+ bytes[9] !== 0x2d ||
118
+ format !== 1 ||
119
+ (endian !== 'v' && endian !== 'V')
120
+ ) {
121
+ throw new DrftError(
122
+ `blend: header "${ascii(bytes, 0, 17)}" is a container layout this reader does not know`,
123
+ );
124
+ }
125
+ return {
126
+ pointerSize: 8,
127
+ littleEndian: endian === 'v',
128
+ version: Number(ascii(bytes, 13, 4)),
129
+ headerSize,
130
+ blockHeaderSize: 32,
131
+ };
132
+ }
133
+
134
+ /** Walk every block header up to `ENDB`. */
135
+ export function readBlendBlocks(bytes: Uint8Array, header: BlendHeader): BlendBlock[] {
136
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
137
+ const le = header.littleEndian;
138
+ const blocks: BlendBlock[] = [];
139
+ let at = header.headerSize;
140
+ while (at + header.blockHeaderSize <= bytes.length) {
141
+ const code = ascii(bytes, at, 4).replace(/\0+$/, '');
142
+ let length: number;
143
+ let address: bigint;
144
+ let sdna: number;
145
+ let count: number;
146
+ if (header.blockHeaderSize === 32) {
147
+ sdna = view.getInt32(at + 4, le);
148
+ address = view.getBigUint64(at + 8, le);
149
+ length = Number(view.getBigInt64(at + 16, le));
150
+ count = Number(view.getBigInt64(at + 24, le));
151
+ } else {
152
+ length = view.getInt32(at + 4, le);
153
+ address =
154
+ header.pointerSize === 8
155
+ ? view.getBigUint64(at + 8, le)
156
+ : BigInt(view.getUint32(at + 8, le));
157
+ const rest = at + 8 + header.pointerSize;
158
+ sdna = view.getInt32(rest, le);
159
+ count = view.getInt32(rest + 4, le);
160
+ }
161
+ if (code === 'ENDB') return blocks;
162
+ const offset = at + header.blockHeaderSize;
163
+ if (length < 0 || offset + length > bytes.length) {
164
+ throw new DrftError(`blend: block "${code}" at ${at} runs past the end of the file`);
165
+ }
166
+ blocks.push({ code, sdna, address, length, count, offset });
167
+ at = offset + length;
168
+ }
169
+ throw new DrftError('blend: the file ends before its ENDB block, so it was not saved completely');
170
+ }
171
+
172
+ /** Parse the `DNA1` block into structs with every field's offset summed. */
173
+ export function readDna(bytes: Uint8Array, block: BlendBlock, header: BlendHeader): DnaStruct[] {
174
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
175
+ const le = header.littleEndian;
176
+ let at = block.offset;
177
+ const end = block.offset + block.length;
178
+ const expect = (tag: string): void => {
179
+ at = block.offset + Math.ceil((at - block.offset) / 4) * 4;
180
+ if (ascii(bytes, at, 4) !== tag) throw new DrftError(`blend: DNA1 has no ${tag} section`);
181
+ at += 4;
182
+ };
183
+ const strings = (): string[] => {
184
+ const count = view.getInt32(at, le);
185
+ at += 4;
186
+ const out: string[] = [];
187
+ for (let i = 0; i < count; i++) {
188
+ const stop = bytes.indexOf(0, at);
189
+ if (stop < 0 || stop >= end) throw new DrftError('blend: DNA1 string table is truncated');
190
+ out.push(ascii(bytes, at, stop - at));
191
+ at = stop + 1;
192
+ }
193
+ return out;
194
+ };
195
+
196
+ expect('SDNA');
197
+ expect('NAME');
198
+ const names = strings();
199
+ expect('TYPE');
200
+ const types = strings();
201
+ expect('TLEN');
202
+ const lengths: number[] = [];
203
+ for (let i = 0; i < types.length; i++) lengths.push(view.getInt16(at + i * 2, le));
204
+ at += types.length * 2;
205
+ expect('STRC');
206
+ const count = view.getInt32(at, le);
207
+ at += 4;
208
+
209
+ const structs: DnaStruct[] = [];
210
+ for (let s = 0; s < count; s++) {
211
+ const typeIndex = view.getInt16(at, le);
212
+ const fieldCount = view.getInt16(at + 2, le);
213
+ at += 4;
214
+ const fields = new Map<string, DnaField>();
215
+ let offset = 0;
216
+ for (let f = 0; f < fieldCount; f++) {
217
+ const typeOf = view.getInt16(at, le);
218
+ const type = types[typeOf] ?? '';
219
+ const raw = names[view.getInt16(at + 2, le)] ?? '';
220
+ at += 4;
221
+ const pointer = raw.startsWith('*') || raw.startsWith('(*');
222
+ const dims = [...raw.matchAll(/\[(\d+)\]/g)].map((m) => Number(m[1]));
223
+ const element = pointer ? header.pointerSize : (lengths[typeOf] ?? 0);
224
+ const size = dims.reduce((n, d) => n * d, element);
225
+ const name = raw.replace(/^[(*]+/, '').replace(/[)[].*$/, '');
226
+ fields.set(name, { name, type, offset, size, pointer, dims });
227
+ offset += size;
228
+ }
229
+ const type = types[typeIndex] ?? '';
230
+ structs.push({ index: s, type, size: lengths[typeIndex] ?? offset, fields });
231
+ }
232
+ return structs;
233
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * A `.blend` mesh as triangle primitives, one per material slot, in glTF's conventions.
3
+ *
4
+ * **The same conversion serves every route out of a `.blend`.** `blendGltf.ts` writes these into a
5
+ * glTF document for `readModel`, and a bake that keeps a world's instancing calls this once per
6
+ * distinct mesh rather than once per object. So what a corner becomes — its position turned Y up,
7
+ * its UV flipped to glTF's origin, its colour linear — is decided here and nowhere else.
8
+ *
9
+ * **Vertices are shared where every attribute agrees exactly**, which is how Blender's exporter
10
+ * builds them, found by walking the exported copies of each source vertex rather than by hashing,
11
+ * so a fifty-million-corner city costs a scan rather than fifty million string keys.
12
+ *
13
+ * **Faces are triangulated as Blender does it**: a quad split from its first corner, an ngon by ear
14
+ * clipping in its own plane. Blender clips ears in a different order, so an ngon's diagonals can
15
+ * differ from Blender's; on a planar face that changes no pixel, on a bent one it changes the bend.
16
+ */
17
+
18
+ import type { BlendMeshData } from './blendMesh.ts';
19
+ import { POINT } from './blendMesh.ts';
20
+ import { faceNormals } from './blendNormals.ts';
21
+
22
+ /** What a material slot asks of the geometry that wears it. */
23
+ export interface SlotNeeds {
24
+ /** Which colour attribute to carry as `COLOR_0`: a name, `''` for the render one, or null. */
25
+ readonly vertexColor: string | null;
26
+ /** Which UV map to carry as `TEXCOORD_0`, by name, or null for the render map. */
27
+ readonly uvMap: string | null;
28
+ /** A 3x3 affine transform applied to that map in Blender's UV space, or null. */
29
+ readonly uvTransform: readonly number[] | null;
30
+ }
31
+
32
+ export interface BlendPrimitive {
33
+ /** The material slot, or -1 where the object has none. */
34
+ readonly slot: number;
35
+ readonly positions: Float32Array;
36
+ readonly normals: Float32Array;
37
+ readonly uvs: Float32Array | null;
38
+ readonly colors: Float32Array | null;
39
+ readonly indices: Uint32Array;
40
+ /** The source vertex each exported vertex came from, for shape keys and weights. */
41
+ readonly sourceVertex: Int32Array;
42
+ /** The source corner each exported vertex first came from, for per-corner shape normals. */
43
+ readonly sourceCorner: Int32Array;
44
+ }
45
+
46
+ /** Blender Z up to glTF Y up: (x, y, z) becomes (x, z, -y). */
47
+ export function toYUp(out: Float32Array, at: number, x: number, y: number, z: number): void {
48
+ out[at] = x;
49
+ out[at + 1] = z;
50
+ out[at + 2] = -y;
51
+ }
52
+
53
+ /** Triangles of one face, as corner indices, appended to `out`. */
54
+ export function triangulateFace(
55
+ mesh: BlendMeshData,
56
+ face: number,
57
+ normal: readonly number[],
58
+ out: number[],
59
+ ): void {
60
+ const start = mesh.faceOffsets[face] as number;
61
+ const end = mesh.faceOffsets[face + 1] as number;
62
+ const n = end - start;
63
+ if (n < 3) return;
64
+ if (n === 3) {
65
+ out.push(start, start + 1, start + 2);
66
+ return;
67
+ }
68
+ if (n === 4) {
69
+ out.push(start, start + 1, start + 2, start, start + 2, start + 3);
70
+ return;
71
+ }
72
+ /* Project onto the face's plane, then clip ears. */
73
+ const [nx, ny, nz] = normal as [number, number, number];
74
+ const ax = Math.abs(nx);
75
+ const ay = Math.abs(ny);
76
+ const az = Math.abs(nz);
77
+ const drop = az >= ax && az >= ay ? 2 : ax >= ay ? 0 : 1;
78
+ const flip = (drop === 2 ? nz : drop === 0 ? nx : ny) < 0;
79
+ const p = mesh.positions;
80
+ const xs: number[] = [];
81
+ const ys: number[] = [];
82
+ for (let c = start; c < end; c++) {
83
+ const v = (mesh.cornerVerts[c] as number) * 3;
84
+ /* The two axes left after dropping one, in right-handed order: (y, z), (z, x) or (x, y). */
85
+ const a = p[v + ((drop + 1) % 3)] as number;
86
+ const b = p[v + ((drop + 2) % 3)] as number;
87
+ xs.push(flip ? b : a);
88
+ ys.push(flip ? a : b);
89
+ }
90
+ const ring = Array.from({ length: n }, (_, i) => i);
91
+ const cross = (i: number, j: number, k: number): number =>
92
+ ((xs[j] as number) - (xs[i] as number)) * ((ys[k] as number) - (ys[i] as number)) -
93
+ ((ys[j] as number) - (ys[i] as number)) * ((xs[k] as number) - (xs[i] as number));
94
+ const inside = (i: number, j: number, k: number, q: number): boolean =>
95
+ cross(i, j, q) >= 0 && cross(j, k, q) >= 0 && cross(k, i, q) >= 0;
96
+ let guard = 0;
97
+ while (ring.length > 3 && guard++ < n * n) {
98
+ let clipped = false;
99
+ for (let r = 0; r < ring.length; r++) {
100
+ const i = ring[(r + ring.length - 1) % ring.length] as number;
101
+ const j = ring[r] as number;
102
+ const k = ring[(r + 1) % ring.length] as number;
103
+ if (cross(i, j, k) <= 0) continue;
104
+ let blocked = false;
105
+ for (const q of ring) {
106
+ if (q !== i && q !== j && q !== k && inside(i, j, k, q)) {
107
+ blocked = true;
108
+ break;
109
+ }
110
+ }
111
+ if (blocked) continue;
112
+ out.push(start + i, start + j, start + k);
113
+ ring.splice(r, 1);
114
+ clipped = true;
115
+ break;
116
+ }
117
+ /* A face too bent or degenerate to clip: fan the rest, which is what remains drawable. */
118
+ if (!clipped) break;
119
+ }
120
+ for (let r = 1; r + 1 < ring.length; r++)
121
+ out.push(
122
+ start + (ring[0] as number),
123
+ start + (ring[r] as number),
124
+ start + (ring[r + 1] as number),
125
+ );
126
+ }
127
+
128
+ /** Split a mesh into one primitive per material slot it uses. */
129
+ export function meshPrimitives(
130
+ mesh: BlendMeshData,
131
+ normals: Float32Array,
132
+ slots: number,
133
+ needs: (slot: number) => SlotNeeds,
134
+ ): BlendPrimitive[] {
135
+ const faces = mesh.faceOffsets.length - 1;
136
+ const face = faceNormals(mesh);
137
+ const bySlot = new Map<number, number[]>();
138
+ for (let f = 0; f < faces; f++) {
139
+ const raw = mesh.materialIndex === null ? 0 : (mesh.materialIndex[f] as number);
140
+ const slot = slots === 0 ? -1 : Math.max(0, Math.min(slots - 1, raw));
141
+ let list = bySlot.get(slot);
142
+ if (list === undefined) bySlot.set(slot, (list = []));
143
+ list.push(f);
144
+ }
145
+ const out: BlendPrimitive[] = [];
146
+ for (const slot of [...bySlot.keys()].sort((a, b) => a - b)) {
147
+ const need = slot < 0 ? { vertexColor: null, uvMap: null, uvTransform: null } : needs(slot);
148
+ const corners: number[] = [];
149
+ for (const f of bySlot.get(slot) as number[])
150
+ triangulateFace(
151
+ mesh,
152
+ f,
153
+ [face[f * 3] as number, face[f * 3 + 1] as number, face[f * 3 + 2] as number],
154
+ corners,
155
+ );
156
+ out.push(build(mesh, normals, slot, need, corners));
157
+ }
158
+ return out;
159
+ }
160
+
161
+ function uvLayer(mesh: BlendMeshData, name: string | null): Float32Array | null {
162
+ if (mesh.uvs.length === 0) return null;
163
+ const found = name === null ? -1 : mesh.uvs.findIndex((u) => u.name === name);
164
+ return (mesh.uvs[found >= 0 ? found : mesh.renderUv] ?? mesh.uvs[0])?.uv ?? null;
165
+ }
166
+
167
+ function colorLayer(
168
+ mesh: BlendMeshData,
169
+ name: string | null,
170
+ ): BlendMeshData['colors'][number] | null {
171
+ if (name === null || mesh.colors.length === 0) return null;
172
+ const found = name === '' ? -1 : mesh.colors.findIndex((c) => c.name === name);
173
+ return mesh.colors[found >= 0 ? found : mesh.renderColor] ?? null;
174
+ }
175
+
176
+ function build(
177
+ mesh: BlendMeshData,
178
+ normals: Float32Array,
179
+ slot: number,
180
+ need: SlotNeeds,
181
+ corners: number[],
182
+ ): BlendPrimitive {
183
+ const uv = uvLayer(mesh, need.uvMap);
184
+ const color = colorLayer(mesh, need.vertexColor);
185
+ const t = need.uvTransform;
186
+ const verts = mesh.positions.length / 3;
187
+ /* Exported copies of each source vertex, as a chain: head per vertex, next per copy. */
188
+ const head = new Int32Array(verts).fill(-1);
189
+ const next: number[] = [];
190
+ const corner: number[] = [];
191
+ const source: number[] = [];
192
+ const indices = new Uint32Array(corners.length);
193
+ const same = (a: number, b: number): boolean => {
194
+ for (let k = 0; k < 3; k++) if (normals[a * 3 + k] !== normals[b * 3 + k]) return false;
195
+ if (uv !== null) for (let k = 0; k < 2; k++) if (uv[a * 2 + k] !== uv[b * 2 + k]) return false;
196
+ if (color !== null && color.domain !== POINT)
197
+ for (let k = 0; k < 4; k++) if (color.rgba[a * 4 + k] !== color.rgba[b * 4 + k]) return false;
198
+ return true;
199
+ };
200
+ for (let i = 0; i < corners.length; i++) {
201
+ const c = corners[i] as number;
202
+ const v = mesh.cornerVerts[c] as number;
203
+ let found = -1;
204
+ for (let e = head[v] as number; e >= 0; e = next[e] as number) {
205
+ if (same(corner[e] as number, c)) {
206
+ found = e;
207
+ break;
208
+ }
209
+ }
210
+ if (found < 0) {
211
+ found = corner.length;
212
+ corner.push(c);
213
+ source.push(v);
214
+ next.push(head[v] as number);
215
+ head[v] = found;
216
+ }
217
+ indices[i] = found;
218
+ }
219
+ const count = corner.length;
220
+ const positions = new Float32Array(count * 3);
221
+ const outNormals = new Float32Array(count * 3);
222
+ const uvs = uv === null ? null : new Float32Array(count * 2);
223
+ const colors = color === null ? null : new Float32Array(count * 4);
224
+ const p = mesh.positions;
225
+ for (let e = 0; e < count; e++) {
226
+ const c = corner[e] as number;
227
+ const v = source[e] as number;
228
+ toYUp(positions, e * 3, p[v * 3] as number, p[v * 3 + 1] as number, p[v * 3 + 2] as number);
229
+ toYUp(
230
+ outNormals,
231
+ e * 3,
232
+ normals[c * 3] as number,
233
+ normals[c * 3 + 1] as number,
234
+ normals[c * 3 + 2] as number,
235
+ );
236
+ if (uvs !== null) {
237
+ let u = uv?.[c * 2] as number;
238
+ let w = uv?.[c * 2 + 1] as number;
239
+ if (t !== null) {
240
+ const tu = (t[0] as number) * u + (t[3] as number) * w + (t[6] as number);
241
+ const tv = (t[1] as number) * u + (t[4] as number) * w + (t[7] as number);
242
+ u = tu;
243
+ w = tv;
244
+ }
245
+ uvs[e * 2] = u;
246
+ uvs[e * 2 + 1] = 1 - w;
247
+ }
248
+ if (colors !== null && color !== null) {
249
+ const at = color.domain === POINT ? v : c;
250
+ for (let k = 0; k < 4; k++) colors[e * 4 + k] = color.rgba[at * 4 + k] as number;
251
+ }
252
+ }
253
+ return {
254
+ slot,
255
+ positions,
256
+ normals: outNormals,
257
+ uvs,
258
+ colors,
259
+ indices,
260
+ sourceVertex: Int32Array.from(source),
261
+ sourceCorner: Int32Array.from(corner),
262
+ };
263
+ }