@sw-file-lib/three 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,1280 @@
1
+ import * as THREE from "three";
2
+ import { BufferAttribute, BufferGeometry } from "three";
3
+ import { WHITE, parseColor } from "@sw-file-lib/core/color";
4
+ import { parseMesh } from "@sw-file-lib/core";
5
+ import { parseComponentDefinitionXml } from "@sw-file-lib/xml";
6
+ import { Orientation, addVec3, mulMat3, mulMat3Vec3, parseMat3, transposeMat3, vec3 } from "@sw-file-lib/core/math";
7
+ import { buildSurfacesGeometry, getSurfaceOrientation, isValidSurfaceOrientation, isValidSurfaceRotation } from "@sw-file-lib/geometry";
8
+ //#region src/geometry.ts
9
+ /**
10
+ * Transfers the contents of a {@link GeometryBuilder} into a `THREE.BufferGeometry`.
11
+ *
12
+ * This function clears the existing attributes and groups of the buffer geometry,
13
+ * then copies the position, normal, color, index, and group data from the builder.
14
+ *
15
+ * @param builder The source geometry builder.
16
+ * @param buffer The target buffer geometry.
17
+ */
18
+ function applyBuilderOnBufferGeometry(builder, buffer) {
19
+ buffer.clearGroups();
20
+ const { position, normal, color, index, groups } = builder.toBufferGeometryAttributes();
21
+ buffer.setAttribute("position", new BufferAttribute(position, 3));
22
+ buffer.setAttribute("normal", new BufferAttribute(normal, 3));
23
+ buffer.setAttribute("color", new BufferAttribute(color.map((v) => v / 255), 4));
24
+ buffer.setIndex(new BufferAttribute(new Uint32Array(index), 1));
25
+ for (const { start, length, materialIndex } of groups) buffer.addGroup(start, length, materialIndex);
26
+ }
27
+ /**
28
+ * Creates a new `THREE.BufferGeometry` from a {@link GeometryBuilder}.
29
+ */
30
+ function bufferGeometryFromBuilder(builder) {
31
+ const buffer = new BufferGeometry();
32
+ applyBuilderOnBufferGeometry(builder, buffer);
33
+ return buffer;
34
+ }
35
+ //#endregion
36
+ //#region src/lights.ts
37
+ const AMBIENT_COLOR_HIGH = new THREE.Color(118 / 255, 142 / 255, 190 / 255);
38
+ const AMBIENT_COLOR_LOW = new THREE.Color(11 / 255, 16 / 255, 44 / 255);
39
+ /**
40
+ * Create the default Stormworks-style light objects.
41
+ *
42
+ * The returned array contains a hemisphere light and a directional light.
43
+ */
44
+ function createSwLights(options = {}) {
45
+ const hemi = new THREE.HemisphereLight(AMBIENT_COLOR_HIGH, AMBIENT_COLOR_LOW, options.hemisphereIntensity ?? 1.6);
46
+ const sun = new THREE.DirectionalLight(new THREE.Color(.95, .95, 1), options.directionalIntensity ?? 1.6);
47
+ sun.position.set(0, 1, -.5);
48
+ return [hemi, sun];
49
+ }
50
+ /** Create a group containing the default Stormworks-style lights. */
51
+ function createSwLightGroup(options = {}) {
52
+ const group = new THREE.Group();
53
+ createSwLights(options).forEach((light) => {
54
+ group.add(light);
55
+ });
56
+ return group;
57
+ }
58
+ //#endregion
59
+ //#region src/material/uniformController.ts
60
+ const CREATE = Symbol("create");
61
+ const ATTACH = Symbol("attach");
62
+ const boolTransformer = {
63
+ toUniform: (v) => v ? 1 : 0,
64
+ fromUniform: (v) => v === 1
65
+ };
66
+ const colorVec3Transformer = {
67
+ toUniform: (v) => [
68
+ v.r / 255,
69
+ v.g / 255,
70
+ v.b / 255
71
+ ],
72
+ fromUniform: (v) => Array.isArray(v) && v.length === 3 && v.every((v) => typeof v === "number") ? {
73
+ r: Math.round(v[0] * 255),
74
+ g: Math.round(v[1] * 255),
75
+ b: Math.round(v[2] * 255)
76
+ } : void 0
77
+ };
78
+ const colorVec4Transformer = {
79
+ toUniform: (v) => [
80
+ v.r / 255,
81
+ v.g / 255,
82
+ v.b / 255,
83
+ v.a !== void 0 ? v.a / 255 : 1
84
+ ],
85
+ fromUniform: (v) => Array.isArray(v) && v.length === 4 && v.every((v) => typeof v === "number") ? {
86
+ r: Math.round(v[0] * 255),
87
+ g: Math.round(v[1] * 255),
88
+ b: Math.round(v[2] * 255),
89
+ a: Math.round(v[3] * 255)
90
+ } : void 0
91
+ };
92
+ /**
93
+ * Manages shader uniforms for a material.
94
+ */
95
+ var UniformController = class UniformController {
96
+ transformers;
97
+ pending = {};
98
+ shaderUniforms;
99
+ /** @hidden */
100
+ static [CREATE](transformers) {
101
+ return new UniformController(transformers);
102
+ }
103
+ constructor(transformers) {
104
+ this.transformers = transformers;
105
+ }
106
+ /** @hidden */
107
+ [ATTACH](shader) {
108
+ this.shaderUniforms = shader;
109
+ for (const key in this.pending) this.set(key, this.pending[key]);
110
+ this.pending = {};
111
+ }
112
+ /** Gets the value of a uniform. */
113
+ get(key) {
114
+ if (this.shaderUniforms) {
115
+ const entry = this.shaderUniforms[key];
116
+ if (entry === void 0) return;
117
+ return this.transformers[key].fromUniform(entry.value);
118
+ } else return this.pending[key];
119
+ }
120
+ /** Sets the value of a uniform. */
121
+ set(key, value) {
122
+ if (!this.shaderUniforms) {
123
+ this.pending[key] = value;
124
+ return;
125
+ }
126
+ const raw = this.transformers[key].toUniform(value);
127
+ const name = key;
128
+ if (this.shaderUniforms[name]) this.shaderUniforms[name].value = raw;
129
+ else this.shaderUniforms[name] = { value: raw };
130
+ }
131
+ /** Patches multiple uniforms at once. */
132
+ patch(values) {
133
+ for (const key in values) this.set(key, values[key]);
134
+ }
135
+ };
136
+ //#endregion
137
+ //#region src/material/additive.ts
138
+ /**
139
+ * Create the default uniforms used for additive materials.
140
+ *
141
+ * An additive material can be created using {@link createAdditiveMaterial}.
142
+ *
143
+ * The default uniforms are:
144
+ * - `overrideColorEnabled`: true
145
+ * - `overrideColor`: white
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * const additiveUniforms = createAdditiveUniforms({
150
+ * overrideColorEnabled: false
151
+ * });
152
+ *
153
+ * const additiveMaterial = createAdditiveMaterial(additiveUniforms);
154
+ * ```
155
+ */
156
+ function createAdditiveUniforms(defaults = {}) {
157
+ const controller = UniformController[CREATE]({
158
+ overrideColorEnabled: boolTransformer,
159
+ overrideColor: colorVec4Transformer
160
+ });
161
+ controller.patch({
162
+ overrideColorEnabled: true,
163
+ overrideColor: WHITE,
164
+ ...defaults
165
+ });
166
+ return controller;
167
+ }
168
+ /**
169
+ * Create an additive material used for materialIndex 2 groups.
170
+ *
171
+ * If you want to use custom uniforms, you can create them using {@link createAdditiveUniforms} and pass them to this function.
172
+ *
173
+ * You can override the material parameters by passing a `THREE.MeshBasicMaterialParameters` object as the second argument.
174
+ *
175
+ * @returns A new `THREE.MeshBasicMaterial` with the specified uniforms, or a default one if not provided.
176
+ */
177
+ function createAdditiveMaterial(uniforms, materialParameters) {
178
+ uniforms ??= createAdditiveUniforms();
179
+ const material = new THREE.MeshBasicMaterial({
180
+ vertexColors: true,
181
+ transparent: true,
182
+ blending: THREE.AdditiveBlending,
183
+ depthWrite: false,
184
+ ...materialParameters
185
+ });
186
+ material.onBeforeCompile = (shader) => {
187
+ uniforms[ATTACH](shader.uniforms);
188
+ shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
189
+ uniform vec4 overrideColor;
190
+ uniform int overrideColorEnabled;`).replace("#include <color_vertex>", `#include <color_vertex>
191
+ if (overrideColorEnabled == 1)
192
+ {
193
+ vColor.r = pow(overrideColor.r, 2.2);
194
+ vColor.g = pow(overrideColor.g, 2.2);
195
+ vColor.b = pow(overrideColor.b, 2.2);
196
+ }`);
197
+ };
198
+ return material;
199
+ }
200
+ //#endregion
201
+ //#region src/material/glass.ts
202
+ const SKY_COLOR_UP = {
203
+ r: 0,
204
+ g: 61,
205
+ b: 182
206
+ };
207
+ const SKY_COLOR_DOWN = {
208
+ r: 139,
209
+ g: 210,
210
+ b: 207
211
+ };
212
+ const GLASS_VERTEX_SHADER = `
213
+ out vec3 vWorldPosition;
214
+ out vec3 vWorldNormal;
215
+
216
+ void main() {
217
+ vec4 worldPosition = modelMatrix * vec4(position, 1.0);
218
+ vWorldPosition = worldPosition.xyz;
219
+ vWorldNormal = normalize(mat3(modelMatrix) * normal);
220
+
221
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
222
+ }
223
+ `;
224
+ const GLASS_FRAGMENT_SHADER = `
225
+ uniform vec3 skyColorUp;
226
+ uniform vec3 skyColorDown;
227
+
228
+ in vec3 vWorldPosition;
229
+ in vec3 vWorldNormal;
230
+
231
+ void main() {
232
+ vec3 cameraToFragment = normalize(vWorldPosition - cameraPosition);
233
+ vec3 reflectedDir = reflect(cameraToFragment, normalize(vWorldNormal));
234
+
235
+ float reflectionFactor = max(0.0, -dot(vWorldNormal, cameraToFragment));
236
+ reflectionFactor = pow(1.0 - reflectionFactor, 5.0);
237
+ reflectionFactor = clamp(reflectionFactor, 0.0, 1.0);
238
+
239
+ float angleFactor = reflectedDir.y * 0.5 + 0.5;
240
+ vec3 skyColor = mix(skyColorDown.rgb, skyColorUp.rgb, angleFactor);
241
+
242
+ gl_FragColor = vec4(skyColor * reflectionFactor * 0.823, 0.0);
243
+ }
244
+ `;
245
+ /**
246
+ * Create the default uniforms used for glass materials.
247
+ *
248
+ * A glass material can be created using {@link createGlassMaterial}.
249
+ *
250
+ * The default uniforms are:
251
+ * - `skyColorUp`: { r: 0, g: 61, b: 182 }
252
+ * - `skyColorDown`: { r: 139, g: 210, b: 207 }
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * const glassUniforms = createGlassUniforms({
257
+ * skyColorUp: { r: 0, g: 61, b: 182 },
258
+ * skyColorDown: { r: 139, g: 210, b: 207 }
259
+ * });
260
+ *
261
+ * const glassMaterial = createGlassMaterial(glassUniforms);
262
+ * ```
263
+ */
264
+ function createGlassUniforms(defaults = {}) {
265
+ const controller = UniformController[CREATE]({
266
+ skyColorUp: colorVec3Transformer,
267
+ skyColorDown: colorVec3Transformer
268
+ });
269
+ controller.patch({
270
+ skyColorUp: SKY_COLOR_UP,
271
+ skyColorDown: SKY_COLOR_DOWN,
272
+ ...defaults
273
+ });
274
+ return controller;
275
+ }
276
+ /**
277
+ * Create the default glass material used for materialIndex 1 groups.
278
+ *
279
+ * If you want to use custom uniforms, you can create them using {@link createGlassUniforms} and pass them to this function.
280
+ *
281
+ * You can override the material parameters by passing a `THREE.ShaderMaterialParameters` object as the second argument.
282
+ *
283
+ * @returns A new `THREE.ShaderMaterial` with the specified uniforms, or a default one if not provided.
284
+ */
285
+ function createGlassMaterial(uniforms, materialParameters) {
286
+ uniforms ??= createGlassUniforms();
287
+ const shaderUniforms = {};
288
+ uniforms[ATTACH](shaderUniforms);
289
+ return new THREE.ShaderMaterial({
290
+ vertexShader: GLASS_VERTEX_SHADER,
291
+ fragmentShader: GLASS_FRAGMENT_SHADER,
292
+ uniforms: shaderUniforms,
293
+ transparent: true,
294
+ depthWrite: false,
295
+ blending: THREE.CustomBlending,
296
+ blendSrc: THREE.OneFactor,
297
+ blendDst: THREE.OneMinusSrcAlphaFactor,
298
+ blendEquation: THREE.AddEquation,
299
+ ...materialParameters
300
+ });
301
+ }
302
+ //#endregion
303
+ //#region src/material/opaque.ts
304
+ /**
305
+ * Create the default uniforms used for opaque materials.
306
+ *
307
+ * An opaque material can be created using {@link createOpaqueMaterial}.
308
+ *
309
+ * The default uniforms are:
310
+ * - `overrideColorEnabled`: true
311
+ * - `overrideColor1`: white
312
+ * - `overrideColor2`: white
313
+ * - `overrideColor3`: white
314
+ *
315
+ * @example
316
+ * ```ts
317
+ * const opaqueUniforms = createOpaqueUniforms({
318
+ * overrideColorEnabled: false
319
+ * });
320
+ *
321
+ * const opaqueMaterial = createOpaqueMaterial(opaqueUniforms);
322
+ * ```
323
+ */
324
+ function createOpaqueUniforms(defaults = {}) {
325
+ const controller = UniformController[CREATE]({
326
+ overrideColorEnabled: boolTransformer,
327
+ overrideColor1: colorVec4Transformer,
328
+ overrideColor2: colorVec4Transformer,
329
+ overrideColor3: colorVec4Transformer
330
+ });
331
+ controller.patch({
332
+ overrideColorEnabled: true,
333
+ overrideColor1: WHITE,
334
+ overrideColor2: WHITE,
335
+ overrideColor3: WHITE,
336
+ ...defaults
337
+ });
338
+ return controller;
339
+ }
340
+ /**
341
+ * Create the default opaque material used for materialIndex 0 groups.
342
+ *
343
+ * If you want to use custom uniforms, you can create them using {@link createOpaqueUniforms} and pass them to this function.
344
+ *
345
+ * You can override the material parameters by passing a `THREE.MeshStandardMaterialParameters` object as the second argument.
346
+ *
347
+ * @returns A new `THREE.MeshStandardMaterial` with the specified uniforms, or a default one if not provided.
348
+ */
349
+ function createOpaqueMaterial(uniforms, materialParameters) {
350
+ uniforms ??= createOpaqueUniforms();
351
+ const material = new THREE.MeshStandardMaterial({
352
+ vertexColors: true,
353
+ roughness: 1,
354
+ metalness: 0,
355
+ ...materialParameters
356
+ });
357
+ material.onBeforeCompile = (shader) => {
358
+ uniforms[ATTACH](shader.uniforms);
359
+ shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
360
+ uniform vec4 overrideColor1;
361
+ uniform vec4 overrideColor2;
362
+ uniform vec4 overrideColor3;
363
+ uniform int overrideColorEnabled;`).replace("#include <color_vertex>", `#ifdef USE_COLOR
364
+ vColor.xyz = color.xyz;
365
+ if (overrideColorEnabled == 1) {
366
+ if (distance(color.rgb, vec3(1.0, 0.494, 0.0)) < 0.01) vColor = overrideColor1;
367
+ else if (distance(color.rgb, vec3(0.608, 0.494, 0.0)) < 0.01) vColor = overrideColor2;
368
+ else if (distance(color.rgb, vec3(0.216, 0.494, 0.0)) < 0.01) vColor = overrideColor3;
369
+ }
370
+ vColor.rgb = pow(vColor.rgb, vec3(2.2));
371
+ #endif`);
372
+ shader.fragmentShader = shader.fragmentShader.replace("#include <aomap_fragment>", `#include <aomap_fragment>
373
+ float dist = length(vViewPosition);
374
+ float incidence = max(dot(geometryNormal, geometryViewDir), 0.0);
375
+ float distanceFactor = 0.05 * (1.0 / max(0.01, dist) - 1.0 / 100.0);
376
+ reflectedLight.directDiffuse += diffuseColor.rgb * incidence * distanceFactor * 16.0;`);
377
+ };
378
+ return material;
379
+ }
380
+ //#endregion
381
+ //#region src/material/materials.ts
382
+ /**
383
+ * Create the default materials used by Stormworks object helpers.
384
+ *
385
+ * A new material set has independent uniform stores, making it suitable for
386
+ * object-local color and shader state. Share the returned set only when objects
387
+ * should intentionally share uniform updates.
388
+ */
389
+ function createSwMaterials(options = {}) {
390
+ const opaqueUniforms = createOpaqueUniforms(options.uniforms?.opaque);
391
+ const glassUniforms = createGlassUniforms(options.uniforms?.glass);
392
+ const additiveUniforms = createAdditiveUniforms(options.uniforms?.additive);
393
+ return {
394
+ opaque: createOpaqueMaterial(opaqueUniforms),
395
+ glass: createGlassMaterial(glassUniforms),
396
+ additive: createAdditiveMaterial(additiveUniforms),
397
+ uniforms: {
398
+ opaque: opaqueUniforms,
399
+ glass: glassUniforms,
400
+ additive: additiveUniforms
401
+ }
402
+ };
403
+ }
404
+ //#endregion
405
+ //#region src/mesh.ts
406
+ /**
407
+ * Create a renderable Three.js mesh from parsed Stormworks `mesh` data.
408
+ *
409
+ * The returned mesh owns its geometry. It uses material groups mapped from the
410
+ * file's group shader ids: opaque, glass, and additive.
411
+ */
412
+ function createSwMesh(mesh, options = {}) {
413
+ const geometry = createSwMeshGeometry(mesh);
414
+ const materials = options.materials ?? createSwMaterials();
415
+ const object = new THREE.Mesh(geometry, [
416
+ materials.opaque,
417
+ materials.glass,
418
+ materials.additive
419
+ ]);
420
+ object.name = options.name ?? "";
421
+ return object;
422
+ }
423
+ /**
424
+ * Create a Three.js group from parsed Stormworks `phys` data.
425
+ *
426
+ * Each physics section becomes a child mesh using the `phys` material from the
427
+ * supplied material set.
428
+ */
429
+ function createSwPhysMeshGroup(mesh, material, options = {}) {
430
+ const group = new THREE.Group();
431
+ mesh.groups.forEach((g) => {
432
+ const geometry = createSwPhysGroupGeometry(g);
433
+ group.add(new THREE.Mesh(geometry, material));
434
+ });
435
+ group.name = options.name ?? "";
436
+ return group;
437
+ }
438
+ /**
439
+ * Create buffer geometry for parsed Stormworks render mesh data.
440
+ *
441
+ * The geometry includes `position`, `normal`, and `color` attributes, and material groups derived from group shader ids.
442
+ */
443
+ function createSwMeshGeometry(mesh) {
444
+ const geometry = new THREE.BufferGeometry();
445
+ const positions = new Float32Array(mesh.vertices.length * 3);
446
+ const normals = new Float32Array(mesh.vertices.length * 3);
447
+ const colors = new Float32Array(mesh.vertices.length * 4);
448
+ mesh.vertices.forEach((vertex, index) => {
449
+ const offset3 = index * 3;
450
+ const offset4 = index * 4;
451
+ positions[offset3] = vertex.position.x;
452
+ positions[offset3 + 1] = vertex.position.y;
453
+ positions[offset3 + 2] = -vertex.position.z;
454
+ normals[offset3] = vertex.normal.x;
455
+ normals[offset3 + 1] = vertex.normal.y;
456
+ normals[offset3 + 2] = -vertex.normal.z;
457
+ colors[offset4] = vertex.color.r / 255;
458
+ colors[offset4 + 1] = vertex.color.g / 255;
459
+ colors[offset4 + 2] = vertex.color.b / 255;
460
+ colors[offset4 + 3] = vertex.color.a !== void 0 ? vertex.color.a / 255 : 1;
461
+ });
462
+ geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
463
+ geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
464
+ geometry.setAttribute("color", new THREE.BufferAttribute(colors, 4));
465
+ geometry.setIndex(mesh.indices);
466
+ setGroups(geometry, mesh);
467
+ geometry.computeBoundingSphere();
468
+ return geometry;
469
+ }
470
+ /**
471
+ * Create buffer geometry for parsed Stormworks phys group.
472
+ *
473
+ * The geometry includes `position` attributes.
474
+ */
475
+ function createSwPhysGroupGeometry(physGroup) {
476
+ const geometry = new THREE.BufferGeometry();
477
+ const positions = new Float32Array(physGroup.vertices.length * 3);
478
+ physGroup.vertices.forEach((vertex, index) => {
479
+ const offset = index * 3;
480
+ positions[offset] = vertex.x;
481
+ positions[offset + 1] = vertex.y;
482
+ positions[offset + 2] = -vertex.z;
483
+ });
484
+ geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
485
+ geometry.setIndex(physGroup.indices.length > 0 ? physGroup.indices : createFallbackTriangleIndices(physGroup.vertices.length));
486
+ geometry.computeVertexNormals();
487
+ geometry.computeBoundingSphere();
488
+ return geometry;
489
+ }
490
+ function createFallbackTriangleIndices(vertexCount) {
491
+ return Array.from({ length: vertexCount }, (_, index) => index);
492
+ }
493
+ function setGroups(geometry, mesh) {
494
+ geometry.clearGroups();
495
+ if (mesh.groups.length === 0) {
496
+ geometry.addGroup(0, mesh.indices.length, 0);
497
+ return;
498
+ }
499
+ mesh.groups.forEach((g) => {
500
+ geometry.addGroup(g.indexBufferStart, g.indexBufferLength, g.materialIndex);
501
+ });
502
+ }
503
+ //#endregion
504
+ //#region src/vehicleAssembler/assetResolver.ts
505
+ /**
506
+ * Creates a {@link VehicleAssetResolver} from raw asset providers.
507
+ *
508
+ * The returned resolver automatically parses XML component definitions,
509
+ * converts Stormworks mesh files into `THREE.BufferGeometry`, and
510
+ * caches the results so each asset is loaded and parsed at most once.
511
+ *
512
+ * This is the recommended way to construct a resolver for {@link VehicleBodyAssembler}.
513
+ *
514
+ * @param getComponentDefinitionFile Returns the XML source of a component
515
+ * definition.
516
+ * @param getMeshFile Returns the binary contents of a Stormworks mesh file.
517
+ * @returns A cached asset resolver.
518
+ */
519
+ function createVehicleAssetResolver(getComponentDefinitionFile, getMeshFile) {
520
+ return {
521
+ resolveComponentDefinition: withCache(async (componentId) => {
522
+ const data = await getComponentDefinitionFile(componentId ?? "01_block");
523
+ if (!data) return void 0;
524
+ return parseComponentDefinitionXml(data);
525
+ }),
526
+ resolveMesh: withCache(async (meshPath) => {
527
+ const data = await getMeshFile(meshPath);
528
+ if (!data) return void 0;
529
+ return createSwMeshGeometry(parseMesh(data));
530
+ })
531
+ };
532
+ }
533
+ /**
534
+ * Wraps an asynchronous resolver with memoization.
535
+ *
536
+ * The returned function caches both completed results and in-flight promises,
537
+ * ensuring that concurrent requests for the same key share a single operation.
538
+ *
539
+ * @param resolver The resolver to memoize.
540
+ * @returns A cached resolver.
541
+ */
542
+ function withCache(resolver) {
543
+ const cache = /* @__PURE__ */ new Map();
544
+ return (key) => {
545
+ let value = cache.get(key);
546
+ if (!value) {
547
+ value = Promise.resolve(resolver(key));
548
+ cache.set(key, value);
549
+ }
550
+ return value;
551
+ };
552
+ }
553
+ //#endregion
554
+ //#region src/internal/index.ts
555
+ function stormworksToThreeMatrix4(rot, pos, posScale = 1) {
556
+ return new THREE.Matrix4().set(rot[0], rot[1], -rot[2], pos.x * posScale, rot[3], rot[4], -rot[5], pos.y * posScale, -rot[6], -rot[7], rot[8], -pos.z * posScale, 0, 0, 0, 1);
557
+ }
558
+ //#endregion
559
+ //#region src/vehicleAssembler/utils.ts
560
+ const ADDITIVE_LUT = new Uint8Array([
561
+ 0,
562
+ 0,
563
+ 0,
564
+ 0,
565
+ 0,
566
+ 0,
567
+ 0,
568
+ 0,
569
+ 0,
570
+ 0,
571
+ 0,
572
+ 0,
573
+ 0,
574
+ 0,
575
+ 0,
576
+ 0,
577
+ 0,
578
+ 0,
579
+ 0,
580
+ 0,
581
+ 0,
582
+ 0,
583
+ 0,
584
+ 0,
585
+ 0,
586
+ 0,
587
+ 0,
588
+ 0,
589
+ 0,
590
+ 0,
591
+ 0,
592
+ 0,
593
+ 0,
594
+ 13,
595
+ 25,
596
+ 35,
597
+ 47,
598
+ 57,
599
+ 66,
600
+ 75,
601
+ 85,
602
+ 93,
603
+ 99,
604
+ 106,
605
+ 113,
606
+ 119,
607
+ 126,
608
+ 130,
609
+ 136,
610
+ 141,
611
+ 145,
612
+ 150,
613
+ 154,
614
+ 158,
615
+ 161,
616
+ 164,
617
+ 168,
618
+ 171,
619
+ 174,
620
+ 177,
621
+ 179,
622
+ 182,
623
+ 184,
624
+ 186,
625
+ 188,
626
+ 191,
627
+ 193,
628
+ 194,
629
+ 196,
630
+ 198,
631
+ 200,
632
+ 201,
633
+ 202,
634
+ 203,
635
+ 204,
636
+ 207,
637
+ 208,
638
+ 209,
639
+ 210,
640
+ 211,
641
+ 211,
642
+ 212,
643
+ 214,
644
+ 215,
645
+ 216,
646
+ 217,
647
+ 217,
648
+ 218,
649
+ 219,
650
+ 219,
651
+ 220,
652
+ 220,
653
+ 222,
654
+ 223,
655
+ 223,
656
+ 224,
657
+ 224,
658
+ 225,
659
+ 225,
660
+ 226,
661
+ 226,
662
+ 227,
663
+ 227,
664
+ 228,
665
+ 228,
666
+ 228,
667
+ 230,
668
+ 230,
669
+ 230,
670
+ 231,
671
+ 231,
672
+ 231,
673
+ 232,
674
+ 232,
675
+ 232,
676
+ 233,
677
+ 233,
678
+ 233,
679
+ 234,
680
+ 234,
681
+ 234,
682
+ 234,
683
+ 235,
684
+ 235,
685
+ 235,
686
+ 235,
687
+ 236,
688
+ 236,
689
+ 236,
690
+ 236,
691
+ 236,
692
+ 238,
693
+ 238,
694
+ 238,
695
+ 238,
696
+ 238,
697
+ 239,
698
+ 239,
699
+ 239,
700
+ 239,
701
+ 239,
702
+ 240,
703
+ 240,
704
+ 240,
705
+ 240,
706
+ 240,
707
+ 240,
708
+ 240,
709
+ 241,
710
+ 241,
711
+ 241,
712
+ 241,
713
+ 241,
714
+ 241,
715
+ 241,
716
+ 242,
717
+ 242,
718
+ 242,
719
+ 242,
720
+ 242,
721
+ 242,
722
+ 242,
723
+ 242,
724
+ 243,
725
+ 243,
726
+ 243,
727
+ 243,
728
+ 243,
729
+ 243,
730
+ 243,
731
+ 243,
732
+ 243,
733
+ 244,
734
+ 244,
735
+ 244,
736
+ 244,
737
+ 244,
738
+ 244,
739
+ 244,
740
+ 244,
741
+ 244,
742
+ 244,
743
+ 244,
744
+ 244,
745
+ 246,
746
+ 246,
747
+ 246,
748
+ 246,
749
+ 246,
750
+ 246,
751
+ 246,
752
+ 246,
753
+ 246,
754
+ 246,
755
+ 246,
756
+ 246,
757
+ 246,
758
+ 247,
759
+ 247,
760
+ 247,
761
+ 247,
762
+ 247,
763
+ 247,
764
+ 247,
765
+ 247,
766
+ 247,
767
+ 247,
768
+ 247,
769
+ 247,
770
+ 247,
771
+ 247,
772
+ 247,
773
+ 247,
774
+ 248,
775
+ 248,
776
+ 248,
777
+ 248,
778
+ 248,
779
+ 248,
780
+ 248,
781
+ 248,
782
+ 248,
783
+ 248,
784
+ 248,
785
+ 248,
786
+ 248,
787
+ 248,
788
+ 248,
789
+ 248,
790
+ 248,
791
+ 248,
792
+ 248,
793
+ 248,
794
+ 249,
795
+ 249,
796
+ 249,
797
+ 249,
798
+ 249,
799
+ 249,
800
+ 249,
801
+ 249,
802
+ 249,
803
+ 249,
804
+ 249,
805
+ 249,
806
+ 249,
807
+ 249,
808
+ 249,
809
+ 249,
810
+ 249,
811
+ 249,
812
+ 249,
813
+ 249,
814
+ 249,
815
+ 249,
816
+ 249
817
+ ]);
818
+ function createMaterialsForComponent(component) {
819
+ const materials = createSwMaterials();
820
+ const bc = component.o?.bc ? parseColor(component.o.bc) : void 0;
821
+ const bc2 = component.o?.bc2 ? parseColor(component.o.bc2) : bc;
822
+ const bc3 = component.o?.bc3 ? parseColor(component.o.bc3) : bc;
823
+ if (bc) materials.uniforms.opaque.set("overrideColor1", bc);
824
+ if (bc2) materials.uniforms.opaque.set("overrideColor2", bc2);
825
+ if (bc3) materials.uniforms.opaque.set("overrideColor3", bc3);
826
+ materials.uniforms.opaque.set("overrideColorEnabled", true);
827
+ const ac = component.o?.ac ? parseColor(component.o.ac) : void 0;
828
+ if (ac) materials.uniforms.additive.set("overrideColor", ac);
829
+ materials.uniforms.additive.set("overrideColorEnabled", true);
830
+ return {
831
+ materials,
832
+ materialArr: [
833
+ materials.opaque,
834
+ materials.glass,
835
+ materials.additive
836
+ ]
837
+ };
838
+ }
839
+ function combineGeometries(geometries, material) {
840
+ const group = new THREE.Group();
841
+ for (const geometry of geometries) if (geometry) group.add(new THREE.Mesh(geometry, material));
842
+ return group;
843
+ }
844
+ //#endregion
845
+ //#region src/vehicleAssembler/specialComponents/microcontroller.ts
846
+ const MICROCONTROLLER_CORNER_MESH = "meshes/component_microprocessor_corner.mesh";
847
+ const MICROCONTROLLER_EDGE_MESH = "meshes/component_microprocessor_edge.mesh";
848
+ const MICROCONTROLLER_TOP_MESH = "meshes/component_microprocessor_top.mesh";
849
+ const MICROCONTROLLER_TOP_CORNER_MESH = "meshes/component_microprocessor_top_corner.mesh";
850
+ const MICROCONTROLLER_TOP_EDGE_MESH = "meshes/component_microprocessor_top_edge.mesh";
851
+ async function assembleMicrocontroller(componentInstance, assets) {
852
+ const microcontroller = componentInstance.o?.microprocessor_definition;
853
+ if (!microcontroller) return;
854
+ const width = microcontroller.width ?? 0;
855
+ const length = microcontroller.length ?? 0;
856
+ if (width <= 0 || length <= 0) return;
857
+ const cornerX = .25 * (width - 1);
858
+ const cornerZ = -.25 * (length - 1);
859
+ const surfaces = [];
860
+ for (let x = 0; x < width; x++) for (let z = 0; z < length; z++) {
861
+ surfaces.push({
862
+ orientation: 3,
863
+ shape: 1,
864
+ position: {
865
+ x,
866
+ y: 0,
867
+ z
868
+ }
869
+ });
870
+ surfaces.push({
871
+ orientation: 2,
872
+ shape: 0,
873
+ position: {
874
+ x,
875
+ y: 0,
876
+ z
877
+ }
878
+ });
879
+ }
880
+ for (let x = 0; x < width; x++) {
881
+ surfaces.push({
882
+ orientation: 5,
883
+ shape: 0,
884
+ position: {
885
+ x,
886
+ y: 0,
887
+ z: 0
888
+ }
889
+ });
890
+ surfaces.push({
891
+ orientation: 4,
892
+ shape: 0,
893
+ position: {
894
+ x,
895
+ y: 0,
896
+ z: length - 1
897
+ }
898
+ });
899
+ }
900
+ for (let z = 0; z < length; z++) {
901
+ surfaces.push({
902
+ orientation: 1,
903
+ shape: 0,
904
+ position: {
905
+ x: 0,
906
+ y: 0,
907
+ z
908
+ }
909
+ });
910
+ surfaces.push({
911
+ orientation: 0,
912
+ shape: 0,
913
+ position: {
914
+ x: width - 1,
915
+ y: 0,
916
+ z
917
+ }
918
+ });
919
+ }
920
+ const { materialArr } = createMaterialsForComponent(componentInstance);
921
+ const group = new THREE.Group();
922
+ const cornerMesh = await assets.resolveMesh(MICROCONTROLLER_CORNER_MESH);
923
+ const topCornerMesh = await assets.resolveMesh(MICROCONTROLLER_TOP_CORNER_MESH);
924
+ if (cornerMesh || topCornerMesh) {
925
+ const corner1 = combineGeometries([cornerMesh, topCornerMesh], materialArr);
926
+ const corner2 = corner1.clone();
927
+ const corner3 = corner1.clone();
928
+ const corner4 = corner1.clone();
929
+ corner2.rotation.y = Math.PI / 2;
930
+ corner3.rotation.y = -Math.PI / 2;
931
+ corner4.rotation.y = Math.PI;
932
+ corner2.position.x = cornerX;
933
+ corner3.position.z = cornerZ;
934
+ corner4.position.x = cornerX;
935
+ corner4.position.z = cornerZ;
936
+ group.add(corner1);
937
+ group.add(corner2);
938
+ group.add(corner3);
939
+ group.add(corner4);
940
+ }
941
+ const edgeMesh = await assets.resolveMesh(MICROCONTROLLER_EDGE_MESH);
942
+ const topEdgeMesh = await assets.resolveMesh(MICROCONTROLLER_TOP_EDGE_MESH);
943
+ if (edgeMesh || topEdgeMesh) {
944
+ const edge = combineGeometries([edgeMesh, topEdgeMesh], materialArr);
945
+ for (let x = 0; x < width - 1; x++) {
946
+ const edge1 = edge.clone();
947
+ const edge2 = edge.clone();
948
+ edge2.rotation.y = Math.PI;
949
+ edge1.position.x = .25 * x + .125;
950
+ edge2.position.x = .25 * x + .125;
951
+ edge2.position.z = cornerZ;
952
+ group.add(edge1);
953
+ group.add(edge2);
954
+ }
955
+ for (let z = 0; z < length - 1; z++) {
956
+ const edge1 = edge.clone();
957
+ const edge2 = edge.clone();
958
+ edge1.rotation.y = -Math.PI / 2;
959
+ edge2.rotation.y = Math.PI / 2;
960
+ edge1.position.z = -(.25 * z + .125);
961
+ edge2.position.z = -(.25 * z + .125);
962
+ edge2.position.x = cornerX;
963
+ group.add(edge1);
964
+ group.add(edge2);
965
+ }
966
+ }
967
+ const topMesh = await assets.resolveMesh(MICROCONTROLLER_TOP_MESH);
968
+ if (topMesh) for (let x = 0; x < width - 1; x++) for (let z = 0; z < length - 1; z++) {
969
+ const top = new THREE.Mesh(topMesh, materialArr);
970
+ top.position.x = .25 * x + .125;
971
+ top.position.z = -(.25 * z + .125);
972
+ group.add(top);
973
+ }
974
+ return {
975
+ surfaces,
976
+ objects: [group]
977
+ };
978
+ }
979
+ //#endregion
980
+ //#region src/vehicleAssembler/specialComponents/paintableSign.ts
981
+ async function assemblePaintableSign(componentInstance, definition) {
982
+ const isNoAdditive = ((definition.flags ?? 0) >> 22 & 1) !== 0;
983
+ let paintableSurface = false;
984
+ const surfaces = definition.surfaces?.map((surface) => {
985
+ if (surface.orientation === 2 && (surface.position?.x ?? 0) === 0 && (surface.position?.y ?? 0) === 0 && (surface.position?.z ?? 0) === 0) {
986
+ paintableSurface = true;
987
+ return {
988
+ ...surface,
989
+ shape: 0
990
+ };
991
+ }
992
+ return surface;
993
+ });
994
+ if (!paintableSurface) return surfaces ? { surfaces } : {};
995
+ const objects = [];
996
+ const geom1 = createPixelGeometry(componentInstance.o?.gc?.split(",").map((c) => parseColor(c)) ?? [], .125);
997
+ const opaqueMaterial = createOpaqueMaterial(createOpaqueUniforms({ overrideColorEnabled: false }));
998
+ objects.push(new THREE.Mesh(geom1, opaqueMaterial));
999
+ if (!isNoAdditive) {
1000
+ const geom2 = createPixelGeometry(componentInstance.o?.gca?.split(",").map((c) => parseColor(c)) ?? [], .125, true);
1001
+ const additiveMaterial = createAdditiveMaterial(createAdditiveUniforms({ overrideColorEnabled: false }));
1002
+ objects.push(new THREE.Mesh(geom2, additiveMaterial));
1003
+ }
1004
+ return {
1005
+ surfaces,
1006
+ objects
1007
+ };
1008
+ }
1009
+ function createPixelGeometry(data, y = 0, isAdditive = false) {
1010
+ const positions = [];
1011
+ const normals = [];
1012
+ const colors = [];
1013
+ const indices = [];
1014
+ for (let z = 0; z < 9; z++) for (let x = 0; x < 9; x++) {
1015
+ const intColor = data[z * 9 + x] ?? {
1016
+ r: 255,
1017
+ g: 255,
1018
+ b: 255
1019
+ };
1020
+ const x1 = .25 * (x / 9) - .125;
1021
+ const x2 = .25 * ((x + 1) / 9) - .125;
1022
+ const z1 = -.25 * (z / 9) + .125;
1023
+ const z2 = -.25 * ((z + 1) / 9) + .125;
1024
+ positions.push(x1, y, z1);
1025
+ positions.push(x2, y, z1);
1026
+ positions.push(x2, y, z2);
1027
+ positions.push(x1, y, z2);
1028
+ for (let i = 0; i < 4; i++) {
1029
+ normals.push(0, 1, 0);
1030
+ colors.push(convertSignColor(intColor.r, isAdditive), convertSignColor(intColor.g, isAdditive), convertSignColor(intColor.b, isAdditive));
1031
+ }
1032
+ const i0 = (z * 9 + x) * 4;
1033
+ const i1 = i0 + 1;
1034
+ const i2 = i0 + 2;
1035
+ const i3 = i0 + 3;
1036
+ indices.push(i0, i1, i2);
1037
+ indices.push(i0, i2, i3);
1038
+ }
1039
+ const geometry = new THREE.BufferGeometry();
1040
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
1041
+ geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
1042
+ geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
1043
+ geometry.setIndex(indices);
1044
+ return geometry;
1045
+ }
1046
+ function convertSignColor(value, isAdditive) {
1047
+ value = Math.min(Math.max(Math.round(value), 0), 255);
1048
+ if (isAdditive) return Math.pow((ADDITIVE_LUT[value] ?? 0) / 255, 2.2);
1049
+ else return value / 255;
1050
+ }
1051
+ //#endregion
1052
+ //#region src/vehicleAssembler/bodyAssembler.ts
1053
+ /** The names of the mesh reference attributes in the component definition XML. */
1054
+ const MESH_ATTRIBUTE_NAMES = [
1055
+ "mesh_data_name",
1056
+ "mesh_0_name",
1057
+ "mesh_1_name",
1058
+ "mesh_2_name"
1059
+ ];
1060
+ /**
1061
+ * Assembles a single Stormworks vehicle body into Three.js objects.
1062
+ *
1063
+ * The assembler collects component geometry and meshes, then produces one
1064
+ * `THREE.Group` containing a culled surface mesh and any additional meshes.
1065
+ *
1066
+ * A single instance should be used for one vehicle body.
1067
+ *
1068
+ * The primary methods most applications should call are {@link appendComponent} and {@link build}.
1069
+ *
1070
+ * @example
1071
+ * ```ts
1072
+ * import * as THREE from "three";
1073
+ * import { parseVehicleXml } from "@sw-file-lib/xml";
1074
+ * import { createVehicleAssetResolver, VehicleBodyAssembler } from "@sw-file-lib/three";
1075
+ *
1076
+ * const assetResolver = createVehicleAssetResolver(
1077
+ * (componentId) =>
1078
+ * fetch(`/rom/data/definitions/${componentId}.xml`)
1079
+ * .then((res) => res.ok ? res.text() : undefined),
1080
+ * (meshPath) =>
1081
+ * fetch(`/rom/${meshPath}`)
1082
+ * .then((res) => res.ok ? res.arrayBuffer() : undefined)
1083
+ * );
1084
+ *
1085
+ * const vehicle = parseVehicleXml(text);
1086
+ * const vehicleGroup = new THREE.Group();
1087
+ *
1088
+ * for (const body of vehicle.bodies ?? []) {
1089
+ * const assembler = new VehicleBodyAssembler(assetResolver);
1090
+ * for (const component of body.components ?? []) {
1091
+ * await assembler.appendComponent(component);
1092
+ * }
1093
+ *
1094
+ * const obj = assembler.build();
1095
+ * vehicleGroup.add(obj);
1096
+ * }
1097
+ *
1098
+ * scene.add(vehicleGroup);
1099
+ * ```
1100
+ */
1101
+ var VehicleBodyAssembler = class {
1102
+ assets;
1103
+ surfaces = [];
1104
+ groups = [];
1105
+ /**
1106
+ * Creates a new body assembler.
1107
+ *
1108
+ * @param assets Callbacks used to resolve component definitions and mesh assets.
1109
+ * Create a resolver with {@link createVehicleAssetResolver}, or make your own.
1110
+ * Wrapping them with a cache is recommended when loading from disk or over the network.
1111
+ */
1112
+ constructor(assets) {
1113
+ this.assets = assets;
1114
+ }
1115
+ /**
1116
+ * Appends a component to the body.
1117
+ *
1118
+ * The component type is detected automatically, and the appropriate builder
1119
+ * is used internally. This is the primary method most applications should call.
1120
+ *
1121
+ * @param componentInstance The component instance from the vehicle body.
1122
+ * Represented by a `<c>` element in the vehicle XML.
1123
+ */
1124
+ async appendComponent(componentInstance) {
1125
+ const definition = await this.assets.resolveComponentDefinition(componentInstance.d);
1126
+ if (!definition) return;
1127
+ switch (definition.type) {
1128
+ case 28: return await this.appendPaintableSign(componentInstance, definition);
1129
+ case 37: return await this.appendMicrocontroller(componentInstance);
1130
+ }
1131
+ return await this.appendStandardComponent(componentInstance, definition);
1132
+ }
1133
+ /**
1134
+ * Appends surfaces and prebuilt objects to the body.
1135
+ *
1136
+ * This is a low-level API intended for custom or unsupported components.
1137
+ * All surfaces and objects must be provided in the component's local
1138
+ * coordinate system; they will be transformed into body space automatically.
1139
+ *
1140
+ * @param componentInstance The component instance from the vehicle body.
1141
+ * Represented by a `<c>` element in the vehicle XML.
1142
+ * @param surfaceDefinitions Surface definitions in local space.
1143
+ * @param objects Additional Three.js objects in local space.
1144
+ */
1145
+ appendRawComponent(componentInstance, surfaceDefinitions, objects) {
1146
+ const componentPosition = vec3(componentInstance.o?.vp);
1147
+ let componentMatrix = [
1148
+ 0,
1149
+ 0,
1150
+ 1,
1151
+ -1,
1152
+ 0,
1153
+ 0,
1154
+ 0,
1155
+ -1,
1156
+ 0
1157
+ ];
1158
+ if (componentInstance.o?.r !== void 0) componentMatrix = parseMat3(componentInstance.o.r) ?? componentMatrix;
1159
+ componentMatrix = transposeMat3(componentMatrix);
1160
+ if (componentInstance.t !== void 0) {
1161
+ if ((componentInstance.t & 1) !== 0) componentMatrix = mulMat3(componentMatrix, Orientation.FlipX.toMat3());
1162
+ if ((componentInstance.t & 2) !== 0) componentMatrix = mulMat3(componentMatrix, Orientation.FlipY.toMat3());
1163
+ if ((componentInstance.t & 4) !== 0) componentMatrix = mulMat3(componentMatrix, Orientation.FlipZ.toMat3());
1164
+ }
1165
+ const blockColor = parseColor(componentInstance.o?.bc ?? "x", {
1166
+ r: 255,
1167
+ g: 255,
1168
+ b: 255
1169
+ });
1170
+ const surfaceColors = componentInstance.o?.sc?.split(",").slice(1).map((c) => parseColor(c)) ?? [];
1171
+ for (const [index, surface] of (surfaceDefinitions ?? []).entries()) {
1172
+ const orientation = surface.orientation ?? 0;
1173
+ const rotation = surface.rotation ?? 0;
1174
+ const shape = surface.shape ?? 0;
1175
+ if (!isValidSurfaceOrientation(orientation)) continue;
1176
+ if (!isValidSurfaceRotation(rotation)) continue;
1177
+ const localPosition = vec3(surface.position);
1178
+ const position = addVec3(componentMatrix ? mulMat3Vec3(componentMatrix, localPosition) : localPosition, componentPosition);
1179
+ let matrix = getSurfaceOrientation(orientation, rotation).toMat3();
1180
+ if (componentMatrix) matrix = mulMat3(componentMatrix, matrix);
1181
+ this.surfaces.push({
1182
+ position,
1183
+ matrix,
1184
+ shape,
1185
+ color: surfaceColors[index] ?? blockColor
1186
+ });
1187
+ }
1188
+ if (objects && objects.length > 0) {
1189
+ const group = new THREE.Group();
1190
+ for (const meshObject of objects) group.add(meshObject);
1191
+ group.matrixAutoUpdate = false;
1192
+ group.matrix.copy(stormworksToThreeMatrix4(componentMatrix, componentPosition, .25));
1193
+ this.groups.push(group);
1194
+ }
1195
+ }
1196
+ /**
1197
+ * Appends a standard component using its component definition.
1198
+ *
1199
+ * This resolves surface definitions and any required mesh assets, transforms
1200
+ * them into world space, and adds them to the current body.
1201
+ *
1202
+ * Most users should prefer {@link appendComponent}, which performs type
1203
+ * detection automatically.
1204
+ *
1205
+ * @param componentInstance The component instance from the vehicle body.
1206
+ * Represented by a `<c>` element in the vehicle XML.
1207
+ * @param definition The component definition for the component instance.
1208
+ * @param meshesToExclude Optional list of mesh attributes to ignore.
1209
+ */
1210
+ async appendStandardComponent(componentInstance, definition, meshesToExclude) {
1211
+ const { materialArr } = createMaterialsForComponent(componentInstance);
1212
+ const meshes = [];
1213
+ for (const meshAttributeName of MESH_ATTRIBUTE_NAMES) {
1214
+ if (meshesToExclude?.includes(meshAttributeName) || !definition?.[meshAttributeName]) continue;
1215
+ const geometry = await this.assets.resolveMesh(definition[meshAttributeName]);
1216
+ if (!geometry) continue;
1217
+ meshes.push(new THREE.Mesh(geometry, materialArr));
1218
+ }
1219
+ this.appendRawComponent(componentInstance, definition?.surfaces, meshes);
1220
+ }
1221
+ /**
1222
+ * Appends a paintable sign component.
1223
+ *
1224
+ * Paintable signs generate their geometry dynamically from the component
1225
+ * properties and do not use a predefined mesh asset.
1226
+ *
1227
+ * @param componentInstance The component instance from the vehicle body.
1228
+ * Represented by a `<c>` element in the vehicle XML.
1229
+ * @param definition The component definition for the component instance.
1230
+ */
1231
+ async appendPaintableSign(componentInstance, definition) {
1232
+ const result = await assemblePaintableSign(componentInstance, definition);
1233
+ if (!result) return;
1234
+ this.appendRawComponent(componentInstance, result.surfaces, result.objects);
1235
+ }
1236
+ /**
1237
+ * Appends a microcontroller component.
1238
+ *
1239
+ * Microcontrollers have variable dimensions and therefore generate their
1240
+ * surfaces and meshes procedurally instead of using a fixed definition.
1241
+ *
1242
+ * @param componentInstance The component instance from the vehicle body.
1243
+ * Represented by a `<c>` element in the vehicle XML.
1244
+ */
1245
+ async appendMicrocontroller(componentInstance) {
1246
+ const result = await assembleMicrocontroller(componentInstance, this.assets);
1247
+ if (!result) return;
1248
+ this.appendRawComponent(componentInstance, result.surfaces, result.objects);
1249
+ }
1250
+ /**
1251
+ * Builds the merged surface mesh for this body using {@link GeometryBuilder}.
1252
+ *
1253
+ * Surface culling is performed before constructing geometry unless explicitly disabled by the options.
1254
+ * The returned geometry does not include any additional meshes.
1255
+ *
1256
+ * @see {@link build} for the complete object including additional meshes.
1257
+ */
1258
+ buildSurfaceMesh(options) {
1259
+ return buildSurfacesGeometry(this.surfaces, options);
1260
+ }
1261
+ /**
1262
+ * Builds the final Three.js object representing this vehicle body.
1263
+ *
1264
+ * The returned group contains the merged surface mesh together with every
1265
+ * additional mesh appended during assembly.
1266
+ *
1267
+ * @returns A complete Three.js group for the body.
1268
+ */
1269
+ build(surfaceOptions) {
1270
+ const group = new THREE.Group();
1271
+ const surfaceGeometry = bufferGeometryFromBuilder(this.buildSurfaceMesh(surfaceOptions));
1272
+ const surfaceMaterial = createOpaqueMaterial(createOpaqueUniforms({ overrideColorEnabled: false }));
1273
+ const surfaceMesh = new THREE.Mesh(surfaceGeometry, surfaceMaterial);
1274
+ group.add(surfaceMesh);
1275
+ for (const childGroup of this.groups) group.add(childGroup);
1276
+ return group;
1277
+ }
1278
+ };
1279
+ //#endregion
1280
+ export { MESH_ATTRIBUTE_NAMES, UniformController, VehicleBodyAssembler, applyBuilderOnBufferGeometry, bufferGeometryFromBuilder, createAdditiveMaterial, createAdditiveUniforms, createGlassMaterial, createGlassUniforms, createOpaqueMaterial, createOpaqueUniforms, createSwLightGroup, createSwLights, createSwMaterials, createSwMesh, createSwMeshGeometry, createSwPhysGroupGeometry, createSwPhysMeshGroup, createVehicleAssetResolver, withCache };