@driftengine/assets 3.61.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.
Files changed (100) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +9 -0
  4. package/dist/assetPath.d.ts +51 -0
  5. package/dist/assetPath.js +59 -0
  6. package/dist/coarseLevel.d.ts +94 -0
  7. package/dist/coarseLevel.js +606 -0
  8. package/dist/dds.d.ts +41 -0
  9. package/dist/dds.js +425 -0
  10. package/dist/drawKey.d.ts +64 -0
  11. package/dist/drawKey.js +42 -0
  12. package/dist/drftLoader.d.ts +420 -0
  13. package/dist/drftLoader.js +996 -0
  14. package/dist/drftTextures.d.ts +87 -0
  15. package/dist/drftTextures.js +138 -0
  16. package/dist/fbx.d.ts +86 -0
  17. package/dist/fbx.js +792 -0
  18. package/dist/fbxHarness.d.ts +68 -0
  19. package/dist/fbxHarness.js +185 -0
  20. package/dist/fbxInflate.d.ts +60 -0
  21. package/dist/fbxInflate.js +204 -0
  22. package/dist/fbxMaterial.d.ts +87 -0
  23. package/dist/fbxMaterial.js +345 -0
  24. package/dist/fbxRig.d.ts +130 -0
  25. package/dist/fbxRig.js +601 -0
  26. package/dist/gltf.d.ts +242 -0
  27. package/dist/gltf.js +904 -0
  28. package/dist/gltfAccessor.d.ts +2 -0
  29. package/dist/gltfAccessor.js +106 -0
  30. package/dist/gltfSkin.d.ts +32 -0
  31. package/dist/gltfSkin.js +144 -0
  32. package/dist/iesPhotometry.d.ts +44 -0
  33. package/dist/iesPhotometry.js +94 -0
  34. package/dist/imageInfo.d.ts +31 -0
  35. package/dist/imageInfo.js +119 -0
  36. package/dist/index.d.ts +58 -0
  37. package/dist/index.js +132 -0
  38. package/dist/kn5.d.ts +158 -0
  39. package/dist/kn5.js +557 -0
  40. package/dist/loadProgress.d.ts +118 -0
  41. package/dist/loadProgress.js +1 -0
  42. package/dist/localise.d.ts +35 -0
  43. package/dist/localise.js +253 -0
  44. package/dist/obj.d.ts +54 -0
  45. package/dist/obj.js +335 -0
  46. package/dist/orient.d.ts +104 -0
  47. package/dist/orient.js +241 -0
  48. package/dist/radianceHdr.d.ts +36 -0
  49. package/dist/radianceHdr.js +172 -0
  50. package/dist/readModel.d.ts +153 -0
  51. package/dist/readModel.js +386 -0
  52. package/dist/recognise.d.ts +38 -0
  53. package/dist/recognise.js +97 -0
  54. package/dist/stl.d.ts +22 -0
  55. package/dist/stl.js +131 -0
  56. package/dist/tangentOrder.d.ts +24 -0
  57. package/dist/tangentOrder.js +31 -0
  58. package/dist/threemf.d.ts +46 -0
  59. package/dist/threemf.js +254 -0
  60. package/dist/uploadBudget.d.ts +82 -0
  61. package/dist/uploadBudget.js +86 -0
  62. package/dist/usd.d.ts +49 -0
  63. package/dist/usd.js +542 -0
  64. package/dist/weld.d.ts +44 -0
  65. package/dist/weld.js +237 -0
  66. package/dist/zip.d.ts +35 -0
  67. package/dist/zip.js +110 -0
  68. package/package.json +61 -0
  69. package/src/assetPath.ts +79 -0
  70. package/src/coarseLevel.ts +720 -0
  71. package/src/dds.ts +507 -0
  72. package/src/drawKey.ts +91 -0
  73. package/src/drftLoader.ts +1197 -0
  74. package/src/drftTextures.ts +153 -0
  75. package/src/fbx.ts +934 -0
  76. package/src/fbxHarness.ts +215 -0
  77. package/src/fbxInflate.ts +258 -0
  78. package/src/fbxMaterial.ts +410 -0
  79. package/src/fbxRig.ts +652 -0
  80. package/src/gltf.ts +1161 -0
  81. package/src/gltfAccessor.ts +127 -0
  82. package/src/gltfSkin.ts +210 -0
  83. package/src/iesPhotometry.ts +136 -0
  84. package/src/imageInfo.ts +130 -0
  85. package/src/index.ts +162 -0
  86. package/src/kn5.ts +705 -0
  87. package/src/loadProgress.ts +131 -0
  88. package/src/localise.ts +275 -0
  89. package/src/obj.ts +386 -0
  90. package/src/orient.ts +262 -0
  91. package/src/radianceHdr.ts +195 -0
  92. package/src/readModel.ts +515 -0
  93. package/src/recognise.ts +124 -0
  94. package/src/stl.ts +147 -0
  95. package/src/tangentOrder.ts +32 -0
  96. package/src/threemf.ts +303 -0
  97. package/src/uploadBudget.ts +90 -0
  98. package/src/usd.ts +642 -0
  99. package/src/weld.ts +264 -0
  100. package/src/zip.ts +130 -0
package/dist/weld.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Collapse a triangle soup into shared vertices.
3
+ *
4
+ * Importers produce one vertex per triangle *corner*, because that is how the source
5
+ * formats index their attributes: FBX and OBJ address positions, normals and UVs through
6
+ * separate index lists, and the only way to flatten that into one array per attribute is
7
+ * to give every corner its own vertex. A real model comes out five to eight times larger
8
+ * than it is — a BMW measured 4.57M corners for perhaps 600k distinct vertices.
9
+ *
10
+ * **Corners are only merged when everything about them matches**, not just the position.
11
+ * Two corners at the same point with different normals are a hard edge, and welding them
12
+ * would round it off — the seam down a car's bonnet would soften into a dent. That is a
13
+ * change to the model, made silently, which is exactly what a reader must not do.
14
+ *
15
+ * Offline work, so it may allocate. Nothing here runs per frame.
16
+ */
17
+ import type { MeshData } from '@driftengine/drft';
18
+ /**
19
+ * Weld one mesh. Returns the same mesh when nothing can be merged.
20
+ *
21
+ * **Every attribute decides identity, and that is a cost as well as a correctness rule.** Two
22
+ * corners alike in position, normal and UV but weighted to different bones are two vertices, and
23
+ * merging them moves geometry with the wrong limb; two alike in everything including their UV but
24
+ * opposite in the bitangent's sign are a mirrored UV shell, and merging them lights one side of
25
+ * the model inside out. Neither is visible in any count a bake prints.
26
+ *
27
+ * What it costs is a file whose tangent frame was *derived* per corner instead of authored: a
28
+ * corner soup gets one triangle's frame per corner, so no two corners agree and the weld that
29
+ * would have merged them cannot. **The way out is not a looser key — it is to derive the frame
30
+ * from the welded mesh**, which is what the baker does as of 3.30.0: it passes
31
+ * `deriveTangents: false` to the reader and calls `deriveTangentsFor` here, afterwards.
32
+ *
33
+ * **How much it costs depends on the unwrap, and two figures in this repository disagreed about
34
+ * it.** The frame is normalised, so two triangles produce different frames only where the
35
+ * *direction* u increases in differs between them. On a separable unwrap — u from x, v from y —
36
+ * every triangle on a flat surface agrees and a derived frame costs almost nothing; a 253-mesh CAD
37
+ * export measured 728,168 vertices against 728,161 with no frame, seven vertices, and that file is
38
+ * not on this machine. On a rotational unwrap the direction turns with position and almost nothing
39
+ * merges: 9,600 corners to 9,482 vertices against 1,681, six different frames at one point at
40
+ * worst, measured in `tangentOrder.test.ts` and reproducible. The order is fixed for the second
41
+ * case; the first is why it went unnoticed.
42
+ */
43
+ export declare function weldMesh(mesh: MeshData): MeshData;
44
+ export declare function dropDefaultAttributes(mesh: MeshData): MeshData;
package/dist/weld.js ADDED
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Collapse a triangle soup into shared vertices.
3
+ *
4
+ * Importers produce one vertex per triangle *corner*, because that is how the source
5
+ * formats index their attributes: FBX and OBJ address positions, normals and UVs through
6
+ * separate index lists, and the only way to flatten that into one array per attribute is
7
+ * to give every corner its own vertex. A real model comes out five to eight times larger
8
+ * than it is — a BMW measured 4.57M corners for perhaps 600k distinct vertices.
9
+ *
10
+ * **Corners are only merged when everything about them matches**, not just the position.
11
+ * Two corners at the same point with different normals are a hard edge, and welding them
12
+ * would round it off — the seam down a car's bonnet would soften into a dent. That is a
13
+ * change to the model, made silently, which is exactly what a reader must not do.
14
+ *
15
+ * Offline work, so it may allocate. Nothing here runs per frame.
16
+ */
17
+ /**
18
+ * How finely positions are compared, in units of the model.
19
+ *
20
+ * Exact bit equality is the wrong test: two corners meant to be the same point routinely
21
+ * differ in the last place after a transform, so exact matching welds almost nothing on
22
+ * exactly the files that need it most. Quantising to a millionth is far below anything a
23
+ * model expresses and far above float noise.
24
+ */
25
+ const QUANTUM = 1e-6;
26
+ function quantise(value) {
27
+ return Math.round(value / QUANTUM);
28
+ }
29
+ /**
30
+ * Every optional attribute, and how many floats each spends on a vertex.
31
+ *
32
+ * **One table, read by both halves of the weld** — the key that decides whether two corners are
33
+ * the same vertex, and the copy that writes the survivors. They were two lists written out by
34
+ * hand, and what that cost is the reason this exists: `MeshData` gained a tangent frame, relief,
35
+ * a rig and morph deltas over four days in August and neither list was told about any of them,
36
+ * so for five releases a merged mesh came back **without** its rig, its tangents and its morph
37
+ * targets, and with corners merged across the bones they were weighted to. Nothing raised: an
38
+ * unskinned mesh is a valid mesh, and it draws at its bind pose.
39
+ *
40
+ * **The `satisfies` is the load-bearing part and not decoration.** It names every optional
41
+ * per-vertex attribute the format has, so the day a new one is added to `MeshData` this file
42
+ * stops compiling until it is listed here. A table that can be forgotten is exactly the thing
43
+ * that was already forgotten twice.
44
+ */
45
+ const ATTRIBUTES = {
46
+ uvs: 2,
47
+ specular: 1,
48
+ roughness: 1,
49
+ grain: 1,
50
+ relief: 1,
51
+ emissiveColor: 3,
52
+ tangents: 4,
53
+ joints: 4,
54
+ weights: 4,
55
+ /*
56
+ * The four-lane per-vertex channel. It joins the key above as well as the copy below, which is
57
+ * the behaviour that matters: two vertices at one position whose sway or sky exposure differ
58
+ * are not the same vertex, and welding them would silently keep whichever came first. A leaf
59
+ * tip merged into the trunk it grows from would stop moving.
60
+ */
61
+ channel: 4,
62
+ };
63
+ const NAMES = Object.keys(ATTRIBUTES);
64
+ /**
65
+ * Weld one mesh. Returns the same mesh when nothing can be merged.
66
+ *
67
+ * **Every attribute decides identity, and that is a cost as well as a correctness rule.** Two
68
+ * corners alike in position, normal and UV but weighted to different bones are two vertices, and
69
+ * merging them moves geometry with the wrong limb; two alike in everything including their UV but
70
+ * opposite in the bitangent's sign are a mirrored UV shell, and merging them lights one side of
71
+ * the model inside out. Neither is visible in any count a bake prints.
72
+ *
73
+ * What it costs is a file whose tangent frame was *derived* per corner instead of authored: a
74
+ * corner soup gets one triangle's frame per corner, so no two corners agree and the weld that
75
+ * would have merged them cannot. **The way out is not a looser key — it is to derive the frame
76
+ * from the welded mesh**, which is what the baker does as of 3.30.0: it passes
77
+ * `deriveTangents: false` to the reader and calls `deriveTangentsFor` here, afterwards.
78
+ *
79
+ * **How much it costs depends on the unwrap, and two figures in this repository disagreed about
80
+ * it.** The frame is normalised, so two triangles produce different frames only where the
81
+ * *direction* u increases in differs between them. On a separable unwrap — u from x, v from y —
82
+ * every triangle on a flat surface agrees and a derived frame costs almost nothing; a 253-mesh CAD
83
+ * export measured 728,168 vertices against 728,161 with no frame, seven vertices, and that file is
84
+ * not on this machine. On a rotational unwrap the direction turns with position and almost nothing
85
+ * merges: 9,600 corners to 9,482 vertices against 1,681, six different frames at one point at
86
+ * worst, measured in `tangentOrder.test.ts` and reproducible. The order is fixed for the second
87
+ * case; the first is why it went unnoticed.
88
+ */
89
+ export function weldMesh(mesh) {
90
+ const vertices = mesh.positions.length / 3;
91
+ if (vertices === 0)
92
+ return mesh;
93
+ const map = new Map();
94
+ const remap = new Uint32Array(vertices);
95
+ let unique = 0;
96
+ /* The attributes this mesh actually carries, resolved once and not per vertex. */
97
+ const present = NAMES.filter((name) => mesh[name] !== undefined);
98
+ /* Three floats per target per vertex, interleaved by vertex: `MeshData.morphTargets`' layout. */
99
+ const morphStride = mesh.morphTargets === undefined ? 0 : (mesh.morphTargetCount ?? 0) * 3;
100
+ for (let i = 0; i < vertices; i++) {
101
+ /*
102
+ * The key names every attribute, because two corners differing in any of them are
103
+ * genuinely different vertices — a shared position with a different normal is a hard
104
+ * edge, and with a different colour is a material boundary.
105
+ */
106
+ let key = `${quantise(mesh.positions[i * 3])},` +
107
+ `${quantise(mesh.positions[i * 3 + 1])},` +
108
+ `${quantise(mesh.positions[i * 3 + 2])}|` +
109
+ `${quantise(mesh.normals[i * 3])},` +
110
+ `${quantise(mesh.normals[i * 3 + 1])},` +
111
+ `${quantise(mesh.normals[i * 3 + 2])}|` +
112
+ `${quantise(mesh.colors[i * 3])},` +
113
+ `${quantise(mesh.colors[i * 3 + 1])},` +
114
+ `${quantise(mesh.colors[i * 3 + 2])}|` +
115
+ `${quantise(mesh.emissive[i])}`;
116
+ for (const name of present) {
117
+ const array = mesh[name];
118
+ const stride = ATTRIBUTES[name];
119
+ key += '|';
120
+ for (let c = 0; c < stride; c++)
121
+ key += `${quantise(array[i * stride + c])},`;
122
+ }
123
+ for (let c = 0; c < morphStride; c++) {
124
+ key += `${quantise(mesh.morphTargets?.[i * morphStride + c])},`;
125
+ }
126
+ const found = map.get(key);
127
+ if (found === undefined) {
128
+ map.set(key, unique);
129
+ remap[i] = unique;
130
+ unique++;
131
+ }
132
+ else {
133
+ remap[i] = found;
134
+ }
135
+ }
136
+ if (unique === vertices)
137
+ return mesh;
138
+ const positions = new Float32Array(unique * 3);
139
+ const normals = new Float32Array(unique * 3);
140
+ const colors = new Float32Array(unique * 3);
141
+ const emissive = new Float32Array(unique);
142
+ const copies = {};
143
+ for (const name of present)
144
+ copies[name] = new Float32Array(unique * ATTRIBUTES[name]);
145
+ const morphTargets = morphStride === 0 ? undefined : new Float32Array(unique * morphStride);
146
+ const written = new Uint8Array(unique);
147
+ for (let i = 0; i < vertices; i++) {
148
+ const to = remap[i];
149
+ if (written[to] === 1)
150
+ continue;
151
+ written[to] = 1;
152
+ for (let c = 0; c < 3; c++) {
153
+ positions[to * 3 + c] = mesh.positions[i * 3 + c];
154
+ normals[to * 3 + c] = mesh.normals[i * 3 + c];
155
+ colors[to * 3 + c] = mesh.colors[i * 3 + c];
156
+ }
157
+ emissive[to] = mesh.emissive[i];
158
+ for (const name of present) {
159
+ const from = mesh[name];
160
+ const into = copies[name];
161
+ const stride = ATTRIBUTES[name];
162
+ for (let c = 0; c < stride; c++)
163
+ into[to * stride + c] = from[i * stride + c];
164
+ }
165
+ if (morphTargets !== undefined) {
166
+ for (let c = 0; c < morphStride; c++) {
167
+ morphTargets[to * morphStride + c] = mesh.morphTargets?.[i * morphStride + c];
168
+ }
169
+ }
170
+ }
171
+ const indices = new Uint32Array(mesh.indices.length);
172
+ for (let i = 0; i < mesh.indices.length; i++) {
173
+ indices[i] = remap[mesh.indices[i]];
174
+ }
175
+ return {
176
+ positions,
177
+ normals,
178
+ colors,
179
+ emissive,
180
+ indices,
181
+ ...copies,
182
+ /* Both or neither: an array of deltas with no count moves nothing, and a count with no
183
+ array is a promise about a buffer that is not there. */
184
+ ...(morphTargets === undefined
185
+ ? {}
186
+ : { morphTargets, morphTargetCount: mesh.morphTargetCount }),
187
+ };
188
+ }
189
+ /**
190
+ * Drop optional attributes that never vary from the engine's own default.
191
+ *
192
+ * An importer that has no material information should not invent one, and a writer should
193
+ * not spend a megabyte storing the same number four million times. `Mesh` already reads an
194
+ * absent attribute as a constant, so leaving it out is not a loss — it is the same shading
195
+ * through a cheaper path, with no buffer, no upload and no per-vertex fetch.
196
+ *
197
+ * The defaults are `Mesh`'s, and they are duplicated here on purpose rather than imported:
198
+ * this is a *bake-time* judgement about what is worth storing, and tying it to a rendering
199
+ * constant would make a change to one silently rewrite files produced by the other.
200
+ */
201
+ const DEFAULT_SPECULAR = 0;
202
+ const DEFAULT_ROUGHNESS = 0.4277;
203
+ const DEFAULT_EMISSIVE_COLOR = -1;
204
+ /** Absent grain means none, so an asset that is nowhere mineral stores nothing about it. */
205
+ const DEFAULT_GRAIN = 0;
206
+ /**
207
+ * Absent relief means a perfectly smooth surface, which is what every reader produces.
208
+ *
209
+ * Listed late and for the same reason the weld's table exists: relief landed on `MeshData` on
210
+ * 2026-08-21 and this function was not told, so a producer filling an array of zeroes stored four
211
+ * bytes a vertex saying "no relief" — the one thing an absent attribute already says.
212
+ */
213
+ const DEFAULT_RELIEF = 0;
214
+ export function dropDefaultAttributes(mesh) {
215
+ const constant = (array, value) => {
216
+ if (array === undefined)
217
+ return false;
218
+ for (let i = 0; i < array.length; i++) {
219
+ if (Math.abs(array[i] - value) > 1e-6)
220
+ return false;
221
+ }
222
+ return true;
223
+ };
224
+ const out = { ...mesh };
225
+ if (constant(mesh.specular, DEFAULT_SPECULAR))
226
+ delete out.specular;
227
+ if (constant(mesh.roughness, DEFAULT_ROUGHNESS))
228
+ delete out.roughness;
229
+ if (constant(mesh.grain, DEFAULT_GRAIN))
230
+ delete out.grain;
231
+ if (constant(mesh.relief, DEFAULT_RELIEF))
232
+ delete out.relief;
233
+ if (constant(mesh.emissiveColor, DEFAULT_EMISSIVE_COLOR)) {
234
+ delete out.emissiveColor;
235
+ }
236
+ return out;
237
+ }
package/dist/zip.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Reading a zip container. One responsibility: name a file inside an archive, get its bytes.
3
+ *
4
+ * Shared by `.usdz` and `.3mf`, which is why it exists as its own module rather than inside
5
+ * either: both are zip archives of a documented layout, and a reader written twice is a
6
+ * reader that disagrees with itself once.
7
+ *
8
+ * **The central directory is read, not the local headers.** A zip can be scanned from the
9
+ * front by walking local file headers, and almost every naive reader does. It is wrong: a
10
+ * local header may declare a size of zero and defer the real figures to a data descriptor
11
+ * *after* the payload, which cannot be found without already knowing where the payload ends.
12
+ * The central directory at the back of the file is the authority, always carries the sizes,
13
+ * and is what the specification says to use.
14
+ *
15
+ * **Inflate is injected**, for the same reason as in `fbx.ts`: this module is part of an
16
+ * engine that runs in a browser and must not reach for Node's `zlib`. `.usdz` never needs it,
17
+ * because the format *requires* its entries to be stored uncompressed so they can be memory
18
+ * mapped, and that is why a USDZ can be read with no decompressor at all. `.3mf` is ordinary
19
+ * deflate and needs one.
20
+ */
21
+ import type { Inflate } from './fbx.ts';
22
+ /** One file in the archive. `bytes` is decompressed and ready to read. */
23
+ export interface ZipEntry {
24
+ readonly name: string;
25
+ readonly bytes: Uint8Array;
26
+ }
27
+ /**
28
+ * Every file in an archive, decompressed.
29
+ *
30
+ * Directory entries are skipped: they are zero-length records whose names end in a slash, and
31
+ * a caller asking for files does not want them.
32
+ */
33
+ export declare function readZip(buffer: ArrayBuffer, inflate?: Inflate | null): ZipEntry[];
34
+ /** The first entry whose name matches, or null. Case-insensitive, since archives disagree. */
35
+ export declare function findEntry(entries: readonly ZipEntry[], match: RegExp): ZipEntry | null;
package/dist/zip.js ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Reading a zip container. One responsibility: name a file inside an archive, get its bytes.
3
+ *
4
+ * Shared by `.usdz` and `.3mf`, which is why it exists as its own module rather than inside
5
+ * either: both are zip archives of a documented layout, and a reader written twice is a
6
+ * reader that disagrees with itself once.
7
+ *
8
+ * **The central directory is read, not the local headers.** A zip can be scanned from the
9
+ * front by walking local file headers, and almost every naive reader does. It is wrong: a
10
+ * local header may declare a size of zero and defer the real figures to a data descriptor
11
+ * *after* the payload, which cannot be found without already knowing where the payload ends.
12
+ * The central directory at the back of the file is the authority, always carries the sizes,
13
+ * and is what the specification says to use.
14
+ *
15
+ * **Inflate is injected**, for the same reason as in `fbx.ts`: this module is part of an
16
+ * engine that runs in a browser and must not reach for Node's `zlib`. `.usdz` never needs it,
17
+ * because the format *requires* its entries to be stored uncompressed so they can be memory
18
+ * mapped, and that is why a USDZ can be read with no decompressor at all. `.3mf` is ordinary
19
+ * deflate and needs one.
20
+ */
21
+ import { DrftError } from '@driftengine/drft';
22
+ /** Signature of a central directory record, little-endian: `PK`. */
23
+ const CENTRAL_SIGNATURE = 0x02014b50;
24
+ /** Signature of the end-of-central-directory record: `PK`. */
25
+ const END_SIGNATURE = 0x06054b50;
26
+ /** The two compression methods that appear in practice. Anything else is refused by number. */
27
+ const STORED = 0;
28
+ const DEFLATED = 8;
29
+ /**
30
+ * Find the end-of-central-directory record, which is the only fixed point in a zip.
31
+ *
32
+ * It sits at the very back, except that it carries a trailing comment of arbitrary length, so
33
+ * it has to be searched for backwards. The comment is at most 65535 bytes by definition, so
34
+ * the search is bounded rather than a scan of the whole file.
35
+ */
36
+ function findEnd(view, length) {
37
+ const earliest = Math.max(0, length - 22 - 0xffff);
38
+ for (let at = length - 22; at >= earliest; at--) {
39
+ if (view.getUint32(at, true) === END_SIGNATURE)
40
+ return at;
41
+ }
42
+ throw new DrftError('zip: no end-of-central-directory record, so this is not a zip archive');
43
+ }
44
+ /**
45
+ * Every file in an archive, decompressed.
46
+ *
47
+ * Directory entries are skipped: they are zero-length records whose names end in a slash, and
48
+ * a caller asking for files does not want them.
49
+ */
50
+ export function readZip(buffer, inflate = null) {
51
+ const bytes = new Uint8Array(buffer);
52
+ const view = new DataView(buffer);
53
+ if (buffer.byteLength < 22)
54
+ throw new DrftError('zip: shorter than an empty archive');
55
+ const end = findEnd(view, buffer.byteLength);
56
+ const count = view.getUint16(end + 10, true);
57
+ let at = view.getUint32(end + 16, true);
58
+ const out = [];
59
+ const decoder = new TextDecoder();
60
+ for (let i = 0; i < count; i++) {
61
+ if (at + 46 > buffer.byteLength || view.getUint32(at, true) !== CENTRAL_SIGNATURE) {
62
+ throw new DrftError(`zip: the central directory entry ${i} is malformed at byte ${at}`);
63
+ }
64
+ const method = view.getUint16(at + 10, true);
65
+ const compressedSize = view.getUint32(at + 20, true);
66
+ const uncompressedSize = view.getUint32(at + 24, true);
67
+ const nameLength = view.getUint16(at + 28, true);
68
+ const extraLength = view.getUint16(at + 30, true);
69
+ const commentLength = view.getUint16(at + 32, true);
70
+ const localAt = view.getUint32(at + 42, true);
71
+ const name = decoder.decode(bytes.subarray(at + 46, at + 46 + nameLength));
72
+ at += 46 + nameLength + extraLength + commentLength;
73
+ /* A directory, which carries no payload and is not a file anybody asked for. */
74
+ if (name.endsWith('/'))
75
+ continue;
76
+ /*
77
+ * The local header's *own* name and extra lengths, which differ from the central
78
+ * directory's. Reusing the central figures here is the classic zip bug: the extra field
79
+ * is routinely a different length in the two places, so the payload starts somewhere
80
+ * else and the entry decompresses to noise.
81
+ */
82
+ if (localAt + 30 > buffer.byteLength || view.getUint32(localAt, true) !== 0x04034b50) {
83
+ throw new DrftError(`zip: "${name}" points at byte ${localAt}, which is not a local header`);
84
+ }
85
+ const localNameLength = view.getUint16(localAt + 26, true);
86
+ const localExtraLength = view.getUint16(localAt + 28, true);
87
+ const from = localAt + 30 + localNameLength + localExtraLength;
88
+ if (from + compressedSize > buffer.byteLength) {
89
+ throw new DrftError(`zip: "${name}" claims ${compressedSize} bytes and runs past the archive`);
90
+ }
91
+ const payload = bytes.subarray(from, from + compressedSize);
92
+ if (method === STORED) {
93
+ out.push({ name, bytes: payload });
94
+ continue;
95
+ }
96
+ if (method === DEFLATED) {
97
+ if (inflate === null) {
98
+ throw new DrftError(`zip: "${name}" is deflate-compressed and this reader was given no decompressor`);
99
+ }
100
+ out.push({ name, bytes: inflate(payload, uncompressedSize) });
101
+ continue;
102
+ }
103
+ throw new DrftError(`zip: "${name}" uses compression method ${method}, and only stored and deflate are read`);
104
+ }
105
+ return out;
106
+ }
107
+ /** The first entry whose name matches, or null. Case-insensitive, since archives disagree. */
108
+ export function findEntry(entries, match) {
109
+ return entries.find((entry) => match.test(entry.name)) ?? null;
110
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@driftengine/assets",
3
+ "version": "3.61.0",
4
+ "description": "Model readers and the streaming loader for the .drft container",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "drift-source": "./src/index.ts",
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json",
16
+ "./*": "./*"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "!src/**/*.test.ts",
22
+ "!src/**/*.test.mjs",
23
+ "!src/**/__snapshots__",
24
+ "README.md",
25
+ "LICENSE",
26
+ "NOTICE"
27
+ ],
28
+ "sideEffects": false,
29
+ "dependencies": {
30
+ "@driftengine/drft": "3.61.0"
31
+ },
32
+ "peerDependencies": {
33
+ "@driftengine/core": "3.61.0"
34
+ },
35
+ "author": "Drift Technologies",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/drftrun/driftengine.git",
39
+ "directory": "packages/assets"
40
+ },
41
+ "homepage": "https://github.com/drftrun/driftengine#readme",
42
+ "bugs": "https://github.com/drftrun/driftengine/issues",
43
+ "keywords": [
44
+ "driftengine",
45
+ "3d",
46
+ "webgl",
47
+ "webgpu",
48
+ "typescript",
49
+ "gltf",
50
+ "fbx",
51
+ "obj",
52
+ "model-loader",
53
+ "streaming"
54
+ ],
55
+ "engines": {
56
+ "node": ">=22.12.0"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ }
61
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Turning a path a model file wrote into paths worth trying. One responsibility: candidates.
3
+ *
4
+ * **No filesystem, no network, no opinion about either.** This is string work only, and it
5
+ * exists because every format has the same problem and none of them solve it: a model names
6
+ * its textures with a path that was true on the machine that exported it, and is true almost
7
+ * nowhere else. Deciding *how* to look is the host's job, because only the host knows
8
+ * whether it is reading a folder, a zip, a URL or a bundle somebody dragged in. Deciding
9
+ * *what to look for* is the same everywhere, so it is written once, here.
10
+ *
11
+ * What goes wrong without it, from the asset this was built against:
12
+ *
13
+ * - `textures\carbon.jpg` uses backslashes, which are a path separator on one platform and
14
+ * an ordinary filename character on the others.
15
+ * - the absolute path beside it is `G:\SketchfabRipper_v1.18.0-b2\tools\blender-292\...`,
16
+ * a drive on somebody else's computer. Trying it cannot succeed and, on a host that maps
17
+ * unknown roots, is the one candidate that could succeed *wrongly*.
18
+ * - the file may have been moved since export, so the basename alone, beside the model or
19
+ * in the folder next to it, is worth trying after the declared path and never before it.
20
+ *
21
+ * Order matters and is the whole value: most specific first, so an asset carrying two
22
+ * `wheel.jpg` files in different folders resolves each to its own rather than to whichever
23
+ * the search happened to reach.
24
+ */
25
+
26
+ /**
27
+ * A texture a model named, with its bytes when the model carried them inline.
28
+ *
29
+ * One type for both cases because the difference is the *format's* business and not the
30
+ * baker's: glTF may embed an image in its binary chunk or in a data URI, and may equally
31
+ * point at a file beside itself, while FBX and OBJ only ever name a path. A reader reports
32
+ * whichever it has, and the resolver looks up only what arrived without bytes.
33
+ */
34
+ export interface AssetReference {
35
+ /** What the source called it. Used to address the texture later, and to find it now. */
36
+ readonly name: string;
37
+ /** Present when the model carried the image itself, so nothing needs resolving. */
38
+ readonly bytes?: Uint8Array;
39
+ }
40
+
41
+ /** Backslashes to forward slashes, and any drive letter or leading root removed. */
42
+ function normalise(declared: string): string {
43
+ const forward = declared.replace(/\\/g, '/');
44
+ /* `C:/x`, `//host/x` and `/x` are all absolute somewhere; keep only the part below them. */
45
+ return forward.replace(/^[a-zA-Z]:\//, '').replace(/^\/+/, '');
46
+ }
47
+
48
+ /** Everything after the last separator. */
49
+ export function basenameOf(declared: string): string {
50
+ const normalised = normalise(declared);
51
+ return normalised.slice(normalised.lastIndexOf('/') + 1);
52
+ }
53
+
54
+ /**
55
+ * Paths to try for a reference, best first, relative to the folder holding the model.
56
+ *
57
+ * Deduplicated, because a texture declared as plain `carbon.jpg` would otherwise produce the
58
+ * same candidate three times, and a host that logs its attempts would report three failures
59
+ * for one missing file.
60
+ *
61
+ * `subfolders` are the conventional places a bundle puts its images. They are tried with the
62
+ * basename only, and last, so they can never outrank a path the file actually stated.
63
+ */
64
+ export function assetCandidates(
65
+ declared: string,
66
+ subfolders: readonly string[] = ['textures', 'Textures', 'tex', 'maps', 'images'],
67
+ ): string[] {
68
+ const normalised = normalise(declared);
69
+ if (normalised === '') return [];
70
+ const base = basenameOf(declared);
71
+
72
+ const out: string[] = [normalised];
73
+ /* A declared path with folders in it may have lost them; the basename beside the model
74
+ is the single most common way an asset arrives after being repacked. */
75
+ if (base !== normalised) out.push(base);
76
+ for (const folder of subfolders) out.push(`${folder}/${base}`);
77
+
78
+ return [...new Set(out)];
79
+ }