@ggez/three-runtime 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.js ADDED
@@ -0,0 +1,2805 @@
1
+ // ../shared/src/utils.ts
2
+ import { Euler, Matrix4, Quaternion, Vector3 } from "three";
3
+ function createBlockoutTextureDataUri(color, edgeColor = "#f5f2ea", edgeThickness = 0.018) {
4
+ const size = 256;
5
+ const frame = Math.max(2, Math.min(6, Math.round(size * edgeThickness)));
6
+ const innerInset = frame + 3;
7
+ const seamInset = innerInset + 5;
8
+ const corner = 18;
9
+ const highlight = mixHexColors(edgeColor, "#ffffff", 0.42);
10
+ const frameColor = mixHexColors(edgeColor, color, 0.12);
11
+ const innerShadow = mixHexColors(edgeColor, color, 0.28);
12
+ const svg = `
13
+ <svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
14
+ <rect width="${size}" height="${size}" rx="${corner}" fill="${color}" />
15
+ <rect x="${frame / 2}" y="${frame / 2}" width="${size - frame}" height="${size - frame}" rx="${corner - 2}" fill="none" stroke="${frameColor}" stroke-width="${frame}" />
16
+ <rect x="${innerInset}" y="${innerInset}" width="${size - innerInset * 2}" height="${size - innerInset * 2}" rx="${corner - 5}" fill="none" stroke="${highlight}" stroke-opacity="0.42" stroke-width="1" />
17
+ <rect x="${seamInset}" y="${seamInset}" width="${size - seamInset * 2}" height="${size - seamInset * 2}" rx="${corner - 9}" fill="none" stroke="${innerShadow}" stroke-opacity="0.12" stroke-width="1" />
18
+ <path d="M ${innerInset} ${size * 0.28} H ${size - innerInset}" stroke="${highlight}" stroke-opacity="0.08" stroke-width="1" />
19
+ <path d="M ${size * 0.28} ${innerInset} V ${size - innerInset}" stroke="${highlight}" stroke-opacity="0.06" stroke-width="1" />
20
+ </svg>
21
+ `.trim();
22
+ return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
23
+ }
24
+ function vec3(x, y, z) {
25
+ return { x, y, z };
26
+ }
27
+ function mixHexColors(left, right, t) {
28
+ const normalizedLeft = normalizeHex(left);
29
+ const normalizedRight = normalizeHex(right);
30
+ const leftValue = Number.parseInt(normalizedLeft.slice(1), 16);
31
+ const rightValue = Number.parseInt(normalizedRight.slice(1), 16);
32
+ const channels = [16, 8, 0].map((shift) => {
33
+ const leftChannel = leftValue >> shift & 255;
34
+ const rightChannel = rightValue >> shift & 255;
35
+ return Math.round(leftChannel + (rightChannel - leftChannel) * t).toString(16).padStart(2, "0");
36
+ });
37
+ return `#${channels.join("")}`;
38
+ }
39
+ function normalizeHex(color) {
40
+ if (/^#[0-9a-f]{6}$/i.test(color)) {
41
+ return color;
42
+ }
43
+ if (/^#[0-9a-f]{3}$/i.test(color)) {
44
+ return `#${color.slice(1).split("").map((channel) => `${channel}${channel}`).join("")}`;
45
+ }
46
+ return "#808080";
47
+ }
48
+ function addVec3(left, right) {
49
+ return vec3(left.x + right.x, left.y + right.y, left.z + right.z);
50
+ }
51
+ function subVec3(left, right) {
52
+ return vec3(left.x - right.x, left.y - right.y, left.z - right.z);
53
+ }
54
+ function scaleVec3(vector, scalar) {
55
+ return vec3(vector.x * scalar, vector.y * scalar, vector.z * scalar);
56
+ }
57
+ function dotVec3(left, right) {
58
+ return left.x * right.x + left.y * right.y + left.z * right.z;
59
+ }
60
+ function crossVec3(left, right) {
61
+ return vec3(
62
+ left.y * right.z - left.z * right.y,
63
+ left.z * right.x - left.x * right.z,
64
+ left.x * right.y - left.y * right.x
65
+ );
66
+ }
67
+ function lengthVec3(vector) {
68
+ return Math.sqrt(dotVec3(vector, vector));
69
+ }
70
+ function normalizeVec3(vector, epsilon = 1e-6) {
71
+ const length = lengthVec3(vector);
72
+ if (length <= epsilon) {
73
+ return vec3(0, 0, 0);
74
+ }
75
+ return scaleVec3(vector, 1 / length);
76
+ }
77
+ function averageVec3(vectors) {
78
+ if (vectors.length === 0) {
79
+ return vec3(0, 0, 0);
80
+ }
81
+ const total = vectors.reduce((sum, vector) => addVec3(sum, vector), vec3(0, 0, 0));
82
+ return scaleVec3(total, 1 / vectors.length);
83
+ }
84
+ var tempPosition = new Vector3();
85
+ var tempQuaternion = new Quaternion();
86
+ var tempScale = new Vector3();
87
+ function composeTransforms(parent, child) {
88
+ const matrix = transformToMatrix(parent).multiply(transformToMatrix(child));
89
+ return matrixToTransform(matrix, child.pivot);
90
+ }
91
+ function resolveSceneGraph(nodes, entities = []) {
92
+ const nodeList = Array.from(nodes);
93
+ const entityList = Array.from(entities);
94
+ const nodesById = new Map(nodeList.map((node) => [node.id, node]));
95
+ const nodeWorldTransforms = /* @__PURE__ */ new Map();
96
+ const entityWorldTransforms = /* @__PURE__ */ new Map();
97
+ const nodeChildrenByParentId = /* @__PURE__ */ new Map();
98
+ const entityChildrenByParentId = /* @__PURE__ */ new Map();
99
+ const rootNodeIds = [];
100
+ const rootEntityIds = [];
101
+ const nodeStack = /* @__PURE__ */ new Set();
102
+ const appendNodeChild = (parentId, childId) => {
103
+ const children = nodeChildrenByParentId.get(parentId);
104
+ if (children) {
105
+ children.push(childId);
106
+ return;
107
+ }
108
+ nodeChildrenByParentId.set(parentId, [childId]);
109
+ };
110
+ const appendEntityChild = (parentId, childId) => {
111
+ const children = entityChildrenByParentId.get(parentId);
112
+ if (children) {
113
+ children.push(childId);
114
+ return;
115
+ }
116
+ entityChildrenByParentId.set(parentId, [childId]);
117
+ };
118
+ const resolveNodeTransform = (node) => {
119
+ const cached = nodeWorldTransforms.get(node.id);
120
+ if (cached) {
121
+ return cached;
122
+ }
123
+ if (nodeStack.has(node.id)) {
124
+ const fallback = structuredClone(node.transform);
125
+ nodeWorldTransforms.set(node.id, fallback);
126
+ return fallback;
127
+ }
128
+ nodeStack.add(node.id);
129
+ const parent = node.parentId && node.parentId !== node.id ? nodesById.get(node.parentId) : void 0;
130
+ const resolved = parent ? composeTransforms(resolveNodeTransform(parent), node.transform) : structuredClone(node.transform);
131
+ nodeWorldTransforms.set(node.id, resolved);
132
+ nodeStack.delete(node.id);
133
+ return resolved;
134
+ };
135
+ nodeList.forEach((node) => {
136
+ resolveNodeTransform(node);
137
+ const hasValidParent = Boolean(
138
+ node.parentId && node.parentId !== node.id && nodesById.has(node.parentId)
139
+ );
140
+ if (hasValidParent) {
141
+ appendNodeChild(node.parentId, node.id);
142
+ return;
143
+ }
144
+ rootNodeIds.push(node.id);
145
+ });
146
+ entityList.forEach((entity) => {
147
+ const parent = entity.parentId && nodesById.has(entity.parentId) ? nodesById.get(entity.parentId) : void 0;
148
+ entityWorldTransforms.set(
149
+ entity.id,
150
+ parent ? composeTransforms(resolveNodeTransform(parent), entity.transform) : structuredClone(entity.transform)
151
+ );
152
+ if (parent) {
153
+ appendEntityChild(parent.id, entity.id);
154
+ return;
155
+ }
156
+ rootEntityIds.push(entity.id);
157
+ });
158
+ return {
159
+ entityChildrenByParentId,
160
+ entityWorldTransforms,
161
+ nodeChildrenByParentId,
162
+ nodeWorldTransforms,
163
+ rootEntityIds,
164
+ rootNodeIds
165
+ };
166
+ }
167
+ function isBrushNode(node) {
168
+ return node.kind === "brush";
169
+ }
170
+ function isMeshNode(node) {
171
+ return node.kind === "mesh";
172
+ }
173
+ function isGroupNode(node) {
174
+ return node.kind === "group";
175
+ }
176
+ function isModelNode(node) {
177
+ return node.kind === "model";
178
+ }
179
+ function isPrimitiveNode(node) {
180
+ return node.kind === "primitive";
181
+ }
182
+ function isInstancingNode(node) {
183
+ return node.kind === "instancing";
184
+ }
185
+ function isInstancingSourceNode(node) {
186
+ return isBrushNode(node) || isMeshNode(node) || isPrimitiveNode(node) || isModelNode(node);
187
+ }
188
+ function resolveInstancingSourceNode(nodes, nodeOrId, maxDepth = 32) {
189
+ const nodesById = new Map(Array.from(nodes, (node) => [node.id, node]));
190
+ let current = typeof nodeOrId === "string" ? nodesById.get(nodeOrId) : nodeOrId;
191
+ let depth = 0;
192
+ while (current && depth <= maxDepth) {
193
+ if (isInstancingSourceNode(current)) {
194
+ return current;
195
+ }
196
+ if (!isInstancingNode(current)) {
197
+ return void 0;
198
+ }
199
+ current = nodesById.get(current.data.sourceNodeId);
200
+ depth += 1;
201
+ }
202
+ return void 0;
203
+ }
204
+ function transformToMatrix(transform) {
205
+ return new Matrix4().compose(
206
+ new Vector3(transform.position.x, transform.position.y, transform.position.z),
207
+ new Quaternion().setFromEuler(new Euler(transform.rotation.x, transform.rotation.y, transform.rotation.z, "XYZ")),
208
+ new Vector3(transform.scale.x, transform.scale.y, transform.scale.z)
209
+ );
210
+ }
211
+ function matrixToTransform(matrix, pivot) {
212
+ matrix.decompose(tempPosition, tempQuaternion, tempScale);
213
+ const rotation = new Euler().setFromQuaternion(tempQuaternion, "XYZ");
214
+ return {
215
+ pivot: pivot ? vec3(pivot.x, pivot.y, pivot.z) : void 0,
216
+ position: vec3(tempPosition.x, tempPosition.y, tempPosition.z),
217
+ rotation: vec3(rotation.x, rotation.y, rotation.z),
218
+ scale: vec3(tempScale.x, tempScale.y, tempScale.z)
219
+ };
220
+ }
221
+
222
+ // src/loader.ts
223
+ import {
224
+ AmbientLight as AmbientLight2,
225
+ BackSide as BackSide2,
226
+ Box3 as Box32,
227
+ BoxGeometry as BoxGeometry2,
228
+ BufferGeometry as BufferGeometry2,
229
+ Color,
230
+ DirectionalLight as DirectionalLight2,
231
+ DoubleSide as DoubleSide2,
232
+ EquirectangularReflectionMapping,
233
+ Euler as Euler3,
234
+ Fog,
235
+ Float32BufferAttribute as Float32BufferAttribute2,
236
+ FrontSide as FrontSide2,
237
+ Group as Group2,
238
+ InstancedMesh as InstancedMesh2,
239
+ HemisphereLight as HemisphereLight2,
240
+ LOD as LOD2,
241
+ Matrix4 as Matrix43,
242
+ Mesh as Mesh2,
243
+ MeshStandardMaterial as MeshStandardMaterial2,
244
+ Object3D as Object3D2,
245
+ PointLight as PointLight2,
246
+ Quaternion as Quaternion3,
247
+ SRGBColorSpace as SRGBColorSpace2,
248
+ SpotLight as SpotLight2,
249
+ TextureLoader as TextureLoader2,
250
+ RepeatWrapping as RepeatWrapping2,
251
+ Vector3 as Vector33
252
+ } from "three";
253
+ import { GLTFLoader as GLTFLoader2 } from "three/examples/jsm/loaders/GLTFLoader.js";
254
+ import { HDRLoader } from "three/examples/jsm/loaders/HDRLoader.js";
255
+ import { MTLLoader as MTLLoader2 } from "three/examples/jsm/loaders/MTLLoader.js";
256
+ import { OBJLoader as OBJLoader2 } from "three/examples/jsm/loaders/OBJLoader.js";
257
+
258
+ // ../runtime-format/src/types.ts
259
+ var RUNTIME_SCENE_FORMAT = "web-hammer-engine";
260
+ var CURRENT_RUNTIME_SCENE_VERSION = 6;
261
+ var MIN_RUNTIME_SCENE_VERSION = 4;
262
+
263
+ // ../runtime-format/src/format.ts
264
+ function isRuntimeScene(value) {
265
+ return validateRuntimeScene(value).ok;
266
+ }
267
+ function validateRuntimeScene(value) {
268
+ const errors = [];
269
+ if (!value || typeof value !== "object") {
270
+ return invalid("Runtime scene must be an object.");
271
+ }
272
+ const candidate = value;
273
+ if (candidate.metadata?.format !== RUNTIME_SCENE_FORMAT) {
274
+ errors.push(`Runtime scene metadata.format must be "${RUNTIME_SCENE_FORMAT}".`);
275
+ }
276
+ if (typeof candidate.metadata?.version !== "number") {
277
+ errors.push("Runtime scene metadata.version must be a number.");
278
+ } else if (candidate.metadata.version < MIN_RUNTIME_SCENE_VERSION) {
279
+ errors.push(`Runtime scene metadata.version must be >= ${MIN_RUNTIME_SCENE_VERSION}.`);
280
+ }
281
+ if (!Array.isArray(candidate.nodes)) {
282
+ errors.push("Runtime scene nodes must be an array.");
283
+ }
284
+ if (!Array.isArray(candidate.assets)) {
285
+ errors.push("Runtime scene assets must be an array.");
286
+ }
287
+ if (!Array.isArray(candidate.materials)) {
288
+ errors.push("Runtime scene materials must be an array.");
289
+ }
290
+ if (!Array.isArray(candidate.entities)) {
291
+ errors.push("Runtime scene entities must be an array.");
292
+ }
293
+ if (!Array.isArray(candidate.layers)) {
294
+ errors.push("Runtime scene layers must be an array.");
295
+ }
296
+ if (!candidate.settings || typeof candidate.settings !== "object") {
297
+ errors.push("Runtime scene settings must be an object.");
298
+ }
299
+ if (errors.length > 0) {
300
+ return {
301
+ errors,
302
+ ok: false
303
+ };
304
+ }
305
+ return {
306
+ errors: [],
307
+ ok: true,
308
+ value: candidate
309
+ };
310
+ }
311
+ function migrateRuntimeScene(scene) {
312
+ const migrated = structuredClone(scene);
313
+ migrated.metadata = {
314
+ ...migrated.metadata,
315
+ format: RUNTIME_SCENE_FORMAT,
316
+ version: CURRENT_RUNTIME_SCENE_VERSION
317
+ };
318
+ return migrated;
319
+ }
320
+ function parseRuntimeScene(text) {
321
+ const parsed = JSON.parse(text);
322
+ const validation = validateRuntimeScene(parsed);
323
+ if (!validation.ok) {
324
+ throw new Error(validation.errors.join(" "));
325
+ }
326
+ return migrateRuntimeScene(validation.value);
327
+ }
328
+ function isRuntimeBundle(value) {
329
+ if (!value || typeof value !== "object") {
330
+ return false;
331
+ }
332
+ const candidate = value;
333
+ return Array.isArray(candidate.files) && isRuntimeScene(candidate.manifest);
334
+ }
335
+ function invalid(message) {
336
+ return {
337
+ errors: [message],
338
+ ok: false
339
+ };
340
+ }
341
+
342
+ // src/object-factory.ts
343
+ import {
344
+ AmbientLight,
345
+ BackSide,
346
+ Box3,
347
+ BoxGeometry,
348
+ BufferGeometry,
349
+ DirectionalLight,
350
+ DoubleSide,
351
+ Euler as Euler2,
352
+ Float32BufferAttribute,
353
+ FrontSide,
354
+ Group,
355
+ HemisphereLight,
356
+ InstancedMesh,
357
+ LOD,
358
+ Matrix4 as Matrix42,
359
+ Mesh,
360
+ MeshStandardMaterial,
361
+ Object3D,
362
+ PointLight,
363
+ Quaternion as Quaternion2,
364
+ RepeatWrapping,
365
+ SRGBColorSpace,
366
+ SpotLight,
367
+ TextureLoader,
368
+ Vector3 as Vector32
369
+ } from "three";
370
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
371
+ import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader.js";
372
+ import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
373
+ var textureLoader = new TextureLoader();
374
+ var gltfLoader = new GLTFLoader();
375
+ var mtlLoader = new MTLLoader();
376
+ var modelTextureLoader = new TextureLoader();
377
+ var tempModelInstanceMatrix = new Matrix42();
378
+ var tempModelChildMatrix = new Matrix42();
379
+ var tempPivotMatrix = new Matrix42();
380
+ var tempInstancePosition = new Vector32();
381
+ var tempInstanceQuaternion = new Quaternion2();
382
+ var tempInstanceScale = new Vector32();
383
+ function createWebHammerSceneObjectFactory(engineScene, options = {}) {
384
+ const resources = {
385
+ assetsById: new Map(engineScene.assets.map((asset) => [asset.id, asset])),
386
+ materialCache: /* @__PURE__ */ new Map(),
387
+ modelTemplateCache: /* @__PURE__ */ new Map(),
388
+ textureCache: /* @__PURE__ */ new Map()
389
+ };
390
+ return {
391
+ createInstancingObjects: () => createInstancingObjects(engineScene, resources, options),
392
+ createNodeObject: (node, overrides) => createObjectForNode(node, resources, options, overrides)
393
+ };
394
+ }
395
+ var createThreeRuntimeObjectFactory = createWebHammerSceneObjectFactory;
396
+ async function createObjectForNode(node, resources, options, overrides) {
397
+ const anchor = new Group();
398
+ const content = new Group();
399
+ const transform = overrides?.transform ?? node.transform;
400
+ const pivot = transform.pivot;
401
+ anchor.name = node.name;
402
+ applyTransform(anchor, transform);
403
+ anchor.userData.webHammer = {
404
+ data: node.data,
405
+ hooks: node.hooks,
406
+ id: node.id,
407
+ kind: node.kind,
408
+ metadata: node.metadata,
409
+ tags: node.tags
410
+ };
411
+ if (pivot) {
412
+ content.position.set(-pivot.x, -pivot.y, -pivot.z);
413
+ }
414
+ anchor.add(content);
415
+ if (node.kind === "light") {
416
+ const light = createThreeLight(node);
417
+ if (light) {
418
+ content.add(light);
419
+ }
420
+ return anchor;
421
+ }
422
+ if (node.kind === "group") {
423
+ return anchor;
424
+ }
425
+ if (node.kind === "instancing") {
426
+ anchor.visible = false;
427
+ return anchor;
428
+ }
429
+ if (node.kind === "model") {
430
+ const modelObject = await createModelObject(node, resources, options);
431
+ const lodObject2 = await createLodObjectForModelNode(node, modelObject, resources, options);
432
+ content.add(lodObject2 ?? modelObject);
433
+ return anchor;
434
+ }
435
+ const lodObject = await createLodObjectForGeometryNode(node, resources, options);
436
+ if (lodObject) {
437
+ content.add(lodObject);
438
+ }
439
+ return anchor;
440
+ }
441
+ async function createInstancingObjects(engineScene, resources, options) {
442
+ const sceneGraph = resolveSceneGraph(engineScene.nodes);
443
+ const batches = /* @__PURE__ */ new Map();
444
+ engineScene.nodes.forEach((node) => {
445
+ if (node.kind !== "instancing") {
446
+ return;
447
+ }
448
+ const sourceNode = resolveInstancingSourceNode(engineScene.nodes, node.data.sourceNodeId);
449
+ if (!sourceNode) {
450
+ return;
451
+ }
452
+ const instances = batches.get(sourceNode.id);
453
+ if (instances) {
454
+ instances.push(node);
455
+ return;
456
+ }
457
+ batches.set(sourceNode.id, [node]);
458
+ });
459
+ const objects = [];
460
+ for (const [sourceNodeId, instances] of batches) {
461
+ const sourceNode = resolveInstancingSourceNode(engineScene.nodes, sourceNodeId);
462
+ if (!sourceNode) {
463
+ continue;
464
+ }
465
+ const exportedSourceNode = engineScene.nodes.find((node) => node.id === sourceNode.id);
466
+ if (!exportedSourceNode || exportedSourceNode.kind === "group" || exportedSourceNode.kind === "instancing" || exportedSourceNode.kind === "light") {
467
+ continue;
468
+ }
469
+ const object = exportedSourceNode.kind === "model" ? await createInstancedObjectForModelNode(exportedSourceNode, instances, sceneGraph, resources, options) : await createInstancedObjectForGeometryNode(exportedSourceNode, instances, sceneGraph, resources, options);
470
+ if (object) {
471
+ objects.push(object);
472
+ }
473
+ }
474
+ return objects;
475
+ }
476
+ async function createInstancedObjectForGeometryNode(sourceNode, instances, sceneGraph, resources, options) {
477
+ const baseGroup = await createInstancedGeometryObject(sourceNode.geometry, sourceNode, instances, sceneGraph, resources, options);
478
+ const lodOptions = resolveSceneLodOptions(options.lod);
479
+ if (!lodOptions || !sourceNode.lods?.length) {
480
+ return baseGroup;
481
+ }
482
+ const lod = new LOD();
483
+ lod.name = `${sourceNode.name}:InstancingLOD`;
484
+ lod.autoUpdate = true;
485
+ lod.addLevel(baseGroup, 0);
486
+ for (const level of sourceNode.lods) {
487
+ const levelGroup = await createInstancedGeometryObject(
488
+ level.geometry,
489
+ sourceNode,
490
+ instances,
491
+ sceneGraph,
492
+ resources,
493
+ options,
494
+ level
495
+ );
496
+ const distance = level.level === "mid" ? lodOptions.midDistance : lodOptions.lowDistance;
497
+ lod.addLevel(levelGroup, distance);
498
+ }
499
+ lod.userData.webHammer = {
500
+ instanceNodeIds: instances.map((instance) => instance.id),
501
+ levelOrder: ["high", ...(sourceNode.lods ?? []).map((level) => level.level)],
502
+ sourceNodeId: sourceNode.id
503
+ };
504
+ return lod;
505
+ }
506
+ async function createInstancedGeometryObject(geometry, sourceNode, instances, sceneGraph, resources, options, lodLevel) {
507
+ const group = new Group();
508
+ const pivot = sourceNode.transform.pivot ?? vec3(0, 0, 0);
509
+ for (const primitive of geometry.primitives) {
510
+ const primitiveGeometry = new BufferGeometry();
511
+ primitiveGeometry.setAttribute("position", new Float32BufferAttribute(primitive.positions, 3));
512
+ primitiveGeometry.setAttribute("normal", new Float32BufferAttribute(primitive.normals, 3));
513
+ if (primitive.uvs.length > 0) {
514
+ primitiveGeometry.setAttribute("uv", new Float32BufferAttribute(primitive.uvs, 2));
515
+ }
516
+ primitiveGeometry.setIndex(primitive.indices);
517
+ primitiveGeometry.computeBoundingBox();
518
+ primitiveGeometry.computeBoundingSphere();
519
+ const material = await createThreeMaterial(primitive.material, resources, options);
520
+ const mesh = new InstancedMesh(primitiveGeometry, material, instances.length);
521
+ mesh.castShadow = options.castShadow ?? true;
522
+ mesh.receiveShadow = options.receiveShadow ?? true;
523
+ mesh.name = `${sourceNode.name}:${lodLevel?.level ?? "high"}:${primitive.material.name}:instanced`;
524
+ mesh.userData.webHammer = {
525
+ instanceNodeIds: instances.map((instance) => instance.id),
526
+ lodLevel: lodLevel?.level ?? "high",
527
+ materialId: primitive.material.id,
528
+ sourceNodeId: sourceNode.id
529
+ };
530
+ instances.forEach((instance, index) => {
531
+ const worldTransform = sceneGraph.nodeWorldTransforms.get(instance.id) ?? instance.transform;
532
+ mesh.setMatrixAt(index, composeGeometryInstanceMatrix(worldTransform, pivot));
533
+ });
534
+ mesh.instanceMatrix.needsUpdate = true;
535
+ group.add(mesh);
536
+ }
537
+ group.name = `${sourceNode.name}:instances`;
538
+ group.userData.webHammer = {
539
+ instanceNodeIds: instances.map((instance) => instance.id),
540
+ sourceNodeId: sourceNode.id
541
+ };
542
+ return group;
543
+ }
544
+ async function createInstancedObjectForModelNode(sourceNode, instances, sceneGraph, resources, options) {
545
+ const baseGroup = await createInstancedModelObject(sourceNode, instances, sceneGraph, resources, options);
546
+ const lodOptions = resolveSceneLodOptions(options.lod);
547
+ if (!lodOptions || !sourceNode.lods?.length) {
548
+ return baseGroup;
549
+ }
550
+ const lod = new LOD();
551
+ lod.name = `${sourceNode.name}:InstancingLOD`;
552
+ lod.autoUpdate = true;
553
+ lod.addLevel(baseGroup, 0);
554
+ for (const level of sourceNode.lods) {
555
+ const levelGroup = await createInstancedModelObject(sourceNode, instances, sceneGraph, resources, options, level);
556
+ const distance = level.level === "mid" ? lodOptions.midDistance : lodOptions.lowDistance;
557
+ lod.addLevel(levelGroup, distance);
558
+ }
559
+ lod.userData.webHammer = {
560
+ instanceNodeIds: instances.map((instance) => instance.id),
561
+ levelOrder: ["high", ...(sourceNode.lods ?? []).map((level) => level.level)],
562
+ sourceNodeId: sourceNode.id
563
+ };
564
+ return lod;
565
+ }
566
+ async function createInstancedModelObject(sourceNode, instances, sceneGraph, resources, options, lodLevel) {
567
+ const group = new Group();
568
+ const template = await loadModelTemplate(resolveModelReference(sourceNode, resources.assetsById, lodLevel), options, resources);
569
+ const templateInstanceNodeIds = instances.map((instance) => instance.id);
570
+ let modelMeshIndex = 0;
571
+ template.updateMatrixWorld(true);
572
+ template.traverse((child) => {
573
+ if (!(child instanceof Mesh) || !(child.geometry instanceof BufferGeometry)) {
574
+ return;
575
+ }
576
+ const instancedMesh = new InstancedMesh(child.geometry, child.material, instances.length);
577
+ instancedMesh.castShadow = options.castShadow ?? true;
578
+ instancedMesh.receiveShadow = options.receiveShadow ?? true;
579
+ instancedMesh.name = `${sourceNode.name}:${lodLevel?.level ?? "high"}:${child.name || modelMeshIndex}:instanced`;
580
+ instancedMesh.userData.webHammer = {
581
+ instanceNodeIds: templateInstanceNodeIds,
582
+ lodLevel: lodLevel?.level ?? "high",
583
+ modelMeshIndex,
584
+ sourceNodeId: sourceNode.id
585
+ };
586
+ tempModelChildMatrix.copy(child.matrixWorld);
587
+ instances.forEach((instance, index) => {
588
+ const worldTransform = sceneGraph.nodeWorldTransforms.get(instance.id) ?? instance.transform;
589
+ tempModelInstanceMatrix.copy(composeTransformMatrix(worldTransform)).multiply(tempModelChildMatrix);
590
+ instancedMesh.setMatrixAt(index, tempModelInstanceMatrix);
591
+ });
592
+ instancedMesh.instanceMatrix.needsUpdate = true;
593
+ group.add(instancedMesh);
594
+ modelMeshIndex += 1;
595
+ });
596
+ group.name = `${sourceNode.name}:instances`;
597
+ group.userData.webHammer = {
598
+ instanceNodeIds: templateInstanceNodeIds,
599
+ sourceNodeId: sourceNode.id
600
+ };
601
+ return group;
602
+ }
603
+ async function createLodObjectForGeometryNode(node, resources, options) {
604
+ const baseGroup = await createGeometryObject(node.geometry, node, resources, options);
605
+ const lodOptions = resolveSceneLodOptions(options.lod);
606
+ if (!lodOptions || !node.lods?.length) {
607
+ return baseGroup;
608
+ }
609
+ const lod = new LOD();
610
+ lod.name = `${node.name}:LOD`;
611
+ lod.autoUpdate = true;
612
+ lod.addLevel(baseGroup, 0);
613
+ for (const level of node.lods) {
614
+ const levelGroup = await createGeometryObject(level.geometry, node, resources, options, level);
615
+ const distance = level.level === "mid" ? lodOptions.midDistance : lodOptions.lowDistance;
616
+ lod.addLevel(levelGroup, distance);
617
+ }
618
+ lod.userData.webHammer = {
619
+ levelOrder: ["high", ...(node.lods ?? []).map((level) => level.level)],
620
+ nodeId: node.id
621
+ };
622
+ return lod;
623
+ }
624
+ async function createLodObjectForModelNode(node, baseModel, resources, options) {
625
+ const lodOptions = resolveSceneLodOptions(options.lod);
626
+ if (!lodOptions || !node.lods?.length) {
627
+ return void 0;
628
+ }
629
+ const lod = new LOD();
630
+ lod.name = `${node.name}:LOD`;
631
+ lod.autoUpdate = true;
632
+ lod.addLevel(baseModel, 0);
633
+ for (const level of node.lods) {
634
+ const levelModel = await createModelObject(node, resources, options, level);
635
+ const distance = level.level === "mid" ? lodOptions.midDistance : lodOptions.lowDistance;
636
+ lod.addLevel(levelModel, distance);
637
+ }
638
+ lod.userData.webHammer = {
639
+ levelOrder: ["high", ...(node.lods ?? []).map((level) => level.level)],
640
+ nodeId: node.id
641
+ };
642
+ return lod;
643
+ }
644
+ async function createGeometryObject(geometry, node, resources, options, lodLevel) {
645
+ const group = new Group();
646
+ const meshes = [];
647
+ for (const primitive of geometry.primitives) {
648
+ const primitiveGeometry = new BufferGeometry();
649
+ primitiveGeometry.setAttribute("position", new Float32BufferAttribute(primitive.positions, 3));
650
+ primitiveGeometry.setAttribute("normal", new Float32BufferAttribute(primitive.normals, 3));
651
+ if (primitive.uvs.length > 0) {
652
+ primitiveGeometry.setAttribute("uv", new Float32BufferAttribute(primitive.uvs, 2));
653
+ }
654
+ primitiveGeometry.setIndex(primitive.indices);
655
+ primitiveGeometry.computeBoundingBox();
656
+ primitiveGeometry.computeBoundingSphere();
657
+ const material = await createThreeMaterial(primitive.material, resources, options);
658
+ const mesh = new Mesh(primitiveGeometry, material);
659
+ mesh.castShadow = options.castShadow ?? true;
660
+ mesh.receiveShadow = options.receiveShadow ?? true;
661
+ mesh.name = `${node.name}:${lodLevel?.level ?? "high"}:${primitive.material.name}`;
662
+ mesh.userData.webHammer = {
663
+ lodLevel: lodLevel?.level ?? "high",
664
+ materialId: primitive.material.id,
665
+ nodeId: node.id
666
+ };
667
+ meshes.push(mesh);
668
+ }
669
+ meshes.forEach((mesh) => {
670
+ group.add(mesh);
671
+ });
672
+ return group;
673
+ }
674
+ async function createThreeMaterial(materialSpec, resources, options) {
675
+ const cached = resources.materialCache.get(materialSpec.id);
676
+ if (cached) {
677
+ return cached;
678
+ }
679
+ const material = new MeshStandardMaterial({
680
+ color: materialSpec.color,
681
+ metalness: materialSpec.metallicFactor,
682
+ roughness: materialSpec.roughnessFactor,
683
+ side: resolveMaterialSide(materialSpec.side)
684
+ });
685
+ if (materialSpec.baseColorTexture) {
686
+ const texture = await loadTexture(materialSpec.baseColorTexture, materialSpec, "baseColorTexture", resources, options);
687
+ texture.colorSpace = SRGBColorSpace;
688
+ material.map = texture;
689
+ }
690
+ if (materialSpec.normalTexture) {
691
+ material.normalMap = await loadTexture(materialSpec.normalTexture, materialSpec, "normalTexture", resources, options);
692
+ }
693
+ if (materialSpec.metallicRoughnessTexture) {
694
+ const ormTexture = await loadTexture(
695
+ materialSpec.metallicRoughnessTexture,
696
+ materialSpec,
697
+ "metallicRoughnessTexture",
698
+ resources,
699
+ options
700
+ );
701
+ material.metalnessMap = ormTexture;
702
+ material.roughnessMap = ormTexture;
703
+ }
704
+ material.name = materialSpec.name;
705
+ material.needsUpdate = true;
706
+ resources.materialCache.set(materialSpec.id, material);
707
+ return material;
708
+ }
709
+ async function loadTexture(path, material, slot, resources, options) {
710
+ const resolvedPath = options.resolveAssetUrl ? await options.resolveAssetUrl({
711
+ kind: "texture",
712
+ material,
713
+ path,
714
+ slot
715
+ }) : path;
716
+ const cacheKey = `${slot}:${resolvedPath}`;
717
+ const cached = resources.textureCache.get(cacheKey);
718
+ if (cached) {
719
+ return cached;
720
+ }
721
+ const pendingTexture = textureLoader.loadAsync(resolvedPath);
722
+ const configuredTexture = pendingTexture.then((texture) => {
723
+ texture.wrapS = RepeatWrapping;
724
+ texture.wrapT = RepeatWrapping;
725
+ return texture;
726
+ });
727
+ resources.textureCache.set(cacheKey, configuredTexture);
728
+ return configuredTexture;
729
+ }
730
+ async function createModelObject(node, resources, options, lodLevel) {
731
+ const template = await loadModelTemplate(resolveModelReference(node, resources.assetsById, lodLevel), options, resources);
732
+ const clone = template.clone(true);
733
+ clone.name = `${node.name}:${lodLevel?.level ?? "high"}`;
734
+ clone.userData.webHammer = {
735
+ ...clone.userData.webHammer ?? {},
736
+ lodLevel: lodLevel?.level ?? "high",
737
+ nodeId: node.id
738
+ };
739
+ clone.traverse((child) => {
740
+ if (child instanceof Mesh) {
741
+ child.castShadow = options.castShadow ?? true;
742
+ child.receiveShadow = options.receiveShadow ?? true;
743
+ }
744
+ });
745
+ return clone;
746
+ }
747
+ function resolveModelReference(node, assetsById, lodLevel) {
748
+ const asset = lodLevel ? assetsById.get(lodLevel.assetId) : assetsById.get(node.data.assetId);
749
+ const modelPath = asset?.path ?? node.data.path;
750
+ return {
751
+ asset,
752
+ assetId: asset?.id ?? (lodLevel ? lodLevel.assetId : node.data.assetId),
753
+ center: readAssetVec3(asset, "nativeCenter"),
754
+ fallbackName: `${node.name}:${lodLevel?.level ?? "high"}:fallback`,
755
+ format: resolveModelFormat(asset?.metadata.modelFormat, modelPath),
756
+ modelPath,
757
+ nodeId: node.id,
758
+ nodeName: node.name,
759
+ texturePath: readAssetString(asset, "texturePath")
760
+ };
761
+ }
762
+ async function loadModelTemplate(reference, options, resources) {
763
+ if (!reference.modelPath) {
764
+ return createMissingModelFallback(reference.asset, reference.fallbackName);
765
+ }
766
+ const resolvedPath = options.resolveAssetUrl ? await options.resolveAssetUrl({
767
+ asset: reference.asset,
768
+ format: reference.format,
769
+ kind: "model",
770
+ node: {
771
+ data: {
772
+ assetId: reference.assetId ?? "",
773
+ path: reference.modelPath
774
+ },
775
+ id: reference.nodeId,
776
+ kind: "model",
777
+ name: reference.nodeName,
778
+ transform: {
779
+ position: vec3(0, 0, 0),
780
+ rotation: vec3(0, 0, 0),
781
+ scale: vec3(1, 1, 1)
782
+ }
783
+ },
784
+ path: reference.modelPath
785
+ }) : reference.modelPath;
786
+ const resolvedTexturePath = reference.texturePath && options.resolveAssetUrl ? await options.resolveAssetUrl({
787
+ kind: "texture",
788
+ material: {
789
+ color: "#ffffff",
790
+ id: `material:model-texture:${reference.nodeId}`,
791
+ metallicFactor: 0,
792
+ name: `${reference.nodeName} Model Texture`,
793
+ roughnessFactor: 1
794
+ },
795
+ path: reference.texturePath,
796
+ slot: "baseColorTexture"
797
+ }) : reference.texturePath;
798
+ const cacheKey = `${reference.format}:${resolvedPath}:${resolvedTexturePath ?? ""}:${readAssetString(reference.asset, "materialMtlText") ?? ""}`;
799
+ const cached = resources.modelTemplateCache.get(cacheKey);
800
+ if (cached) {
801
+ return cached;
802
+ }
803
+ const pending = (async () => {
804
+ try {
805
+ const object = reference.format === "obj" ? await loadObjModel(reference.asset, resolvedPath, resolvedTexturePath) : await loadGltfModel(reference.asset, resolvedPath);
806
+ centerObject(object, reference.center);
807
+ return object;
808
+ } catch {
809
+ return createMissingModelFallback(reference.asset, reference.fallbackName);
810
+ }
811
+ })();
812
+ resources.modelTemplateCache.set(cacheKey, pending);
813
+ return pending;
814
+ }
815
+ async function loadObjModel(asset, resolvedPath, resolvedTexturePath) {
816
+ const objLoader = new OBJLoader();
817
+ const mtlText = readAssetString(asset, "materialMtlText");
818
+ if (mtlText) {
819
+ const materialCreator = mtlLoader.parse(patchMtlTextureReferences(mtlText, resolvedTexturePath), "");
820
+ materialCreator.preload();
821
+ objLoader.setMaterials(materialCreator);
822
+ }
823
+ const object = await objLoader.loadAsync(resolvedPath);
824
+ if (!mtlText && resolvedTexturePath) {
825
+ const texture = await loadModelTexture(resolvedTexturePath);
826
+ object.traverse((child) => {
827
+ if (child instanceof Mesh) {
828
+ child.material = new MeshStandardMaterial({
829
+ map: texture,
830
+ metalness: 0.12,
831
+ roughness: 0.76
832
+ });
833
+ }
834
+ });
835
+ }
836
+ return object;
837
+ }
838
+ async function loadGltfModel(asset, resolvedPath) {
839
+ const gltf = await gltfLoader.loadAsync(resolvedPath);
840
+ const object = gltf.scene;
841
+ object.userData.webHammer = {
842
+ ...object.userData.webHammer ?? {},
843
+ animations: gltf.animations,
844
+ nativeCenter: readAssetVec3(asset, "nativeCenter")
845
+ };
846
+ return object;
847
+ }
848
+ async function loadModelTexture(path) {
849
+ const texture = await modelTextureLoader.loadAsync(path);
850
+ texture.wrapS = RepeatWrapping;
851
+ texture.wrapT = RepeatWrapping;
852
+ texture.colorSpace = SRGBColorSpace;
853
+ return texture;
854
+ }
855
+ function centerObject(object, center) {
856
+ const resolvedCenter = center ?? computeObjectCenter(object);
857
+ object.position.set(-resolvedCenter.x, -resolvedCenter.y, -resolvedCenter.z);
858
+ }
859
+ function computeObjectCenter(object) {
860
+ const box = new Box3().setFromObject(object);
861
+ const center = box.getCenter(new Vector32());
862
+ return {
863
+ x: center.x,
864
+ y: center.y,
865
+ z: center.z
866
+ };
867
+ }
868
+ function createMissingModelFallback(asset, name = "Missing Model") {
869
+ const previewColor = readAssetString(asset, "previewColor") ?? "#7f8ea3";
870
+ const size = readAssetVec3(asset, "nativeSize") ?? { x: 1.4, y: 1.4, z: 1.4 };
871
+ const geometry = new BoxGeometry(size.x, size.y, size.z);
872
+ const material = new MeshStandardMaterial({
873
+ color: previewColor,
874
+ metalness: 0.08,
875
+ roughness: 0.72
876
+ });
877
+ const mesh = new Mesh(geometry, material);
878
+ mesh.name = name;
879
+ return mesh;
880
+ }
881
+ function createThreeLight(node) {
882
+ if (!node.data.enabled) {
883
+ return void 0;
884
+ }
885
+ switch (node.data.type) {
886
+ case "ambient": {
887
+ return new AmbientLight(node.data.color, node.data.intensity);
888
+ }
889
+ case "hemisphere": {
890
+ return new HemisphereLight(node.data.color, node.data.groundColor ?? "#0f1721", node.data.intensity);
891
+ }
892
+ case "point": {
893
+ const light = new PointLight(node.data.color, node.data.intensity, node.data.distance ?? 0, node.data.decay ?? 2);
894
+ light.castShadow = node.data.castShadow;
895
+ return light;
896
+ }
897
+ case "directional": {
898
+ const group = new Group();
899
+ const light = new DirectionalLight(node.data.color, node.data.intensity);
900
+ const target = new Object3D();
901
+ light.castShadow = node.data.castShadow;
902
+ target.position.set(0, 0, -6);
903
+ group.add(target);
904
+ group.add(light);
905
+ light.target = target;
906
+ return group;
907
+ }
908
+ case "spot": {
909
+ const group = new Group();
910
+ const light = new SpotLight(
911
+ node.data.color,
912
+ node.data.intensity,
913
+ node.data.distance,
914
+ node.data.angle,
915
+ node.data.penumbra,
916
+ node.data.decay
917
+ );
918
+ const target = new Object3D();
919
+ light.castShadow = node.data.castShadow;
920
+ target.position.set(0, 0, -6);
921
+ group.add(target);
922
+ group.add(light);
923
+ light.target = target;
924
+ return group;
925
+ }
926
+ default:
927
+ return void 0;
928
+ }
929
+ }
930
+ function applyTransform(object, transform) {
931
+ object.position.set(transform.position.x, transform.position.y, transform.position.z);
932
+ object.rotation.set(transform.rotation.x, transform.rotation.y, transform.rotation.z);
933
+ object.scale.set(transform.scale.x, transform.scale.y, transform.scale.z);
934
+ }
935
+ function composeGeometryInstanceMatrix(transform, pivot) {
936
+ return composeTransformMatrix(transform).multiply(
937
+ tempPivotMatrix.makeTranslation(-pivot.x, -pivot.y, -pivot.z)
938
+ );
939
+ }
940
+ function composeTransformMatrix(transform) {
941
+ tempInstancePosition.set(transform.position.x, transform.position.y, transform.position.z);
942
+ tempInstanceQuaternion.setFromEuler(new Euler2(transform.rotation.x, transform.rotation.y, transform.rotation.z, "XYZ"));
943
+ tempInstanceScale.set(transform.scale.x, transform.scale.y, transform.scale.z);
944
+ return new Matrix42().compose(tempInstancePosition, tempInstanceQuaternion, tempInstanceScale);
945
+ }
946
+ function resolveSceneLodOptions(lod) {
947
+ if (!lod) {
948
+ return void 0;
949
+ }
950
+ const midDistance = Math.max(0, lod.midDistance);
951
+ const lowDistance = Math.max(midDistance + 0.01, lod.lowDistance);
952
+ return {
953
+ lowDistance,
954
+ midDistance
955
+ };
956
+ }
957
+ function resolveMaterialSide(side) {
958
+ switch (side) {
959
+ case "back":
960
+ return BackSide;
961
+ case "double":
962
+ return DoubleSide;
963
+ default:
964
+ return FrontSide;
965
+ }
966
+ }
967
+ function resolveModelFormat(format, path) {
968
+ if (typeof format === "string" && format.toLowerCase() === "obj") {
969
+ return "obj";
970
+ }
971
+ return path?.toLowerCase().endsWith(".obj") ? "obj" : "gltf";
972
+ }
973
+ function readAssetString(asset, key) {
974
+ const value = asset?.metadata[key];
975
+ return typeof value === "string" ? value : void 0;
976
+ }
977
+ function readAssetVec3(asset, keyPrefix) {
978
+ const x = asset?.metadata[`${keyPrefix}X`];
979
+ const y = asset?.metadata[`${keyPrefix}Y`];
980
+ const z = asset?.metadata[`${keyPrefix}Z`];
981
+ if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number") {
982
+ return void 0;
983
+ }
984
+ return { x, y, z };
985
+ }
986
+ function patchMtlTextureReferences(mtlText, texturePath) {
987
+ if (!texturePath) {
988
+ return mtlText;
989
+ }
990
+ const mapPattern = /^(map_Ka|map_Kd|map_d|map_Bump|bump)\s+.+$/gm;
991
+ const hasDiffuseMap = /^map_Kd\s+.+$/m.test(mtlText);
992
+ const normalized = mtlText.replace(mapPattern, (line) => {
993
+ if (line.startsWith("map_Kd ")) {
994
+ return `map_Kd ${texturePath}`;
995
+ }
996
+ return line;
997
+ });
998
+ return hasDiffuseMap ? normalized : `${normalized.trim()}
999
+ map_Kd ${texturePath}
1000
+ `;
1001
+ }
1002
+ function extractPhysics(node) {
1003
+ if (node.kind === "primitive") {
1004
+ return node.data.physics;
1005
+ }
1006
+ if (node.kind === "mesh") {
1007
+ return node.data.physics;
1008
+ }
1009
+ return void 0;
1010
+ }
1011
+ function findPrimaryLight(object) {
1012
+ if ("isLight" in object && object.isLight) {
1013
+ return object;
1014
+ }
1015
+ return object.children.find((child) => "isLight" in child && child.isLight);
1016
+ }
1017
+
1018
+ // src/loader.ts
1019
+ var textureLoader2 = new TextureLoader2();
1020
+ var gltfLoader2 = new GLTFLoader2();
1021
+ var hdrLoader = new HDRLoader();
1022
+ var mtlLoader2 = new MTLLoader2();
1023
+ function isWebHammerEngineScene(value) {
1024
+ return isRuntimeScene(value);
1025
+ }
1026
+ function isWebHammerEngineBundle(value) {
1027
+ return isRuntimeBundle(value);
1028
+ }
1029
+ function parseWebHammerEngineScene(text) {
1030
+ return parseRuntimeScene(text);
1031
+ }
1032
+ async function fetchWebHammerEngineScene(url, init) {
1033
+ const response = await fetch(url, init);
1034
+ if (!response.ok) {
1035
+ throw new Error(`Failed to fetch engine scene: ${response.status} ${response.statusText}`);
1036
+ }
1037
+ return parseWebHammerEngineScene(await response.text());
1038
+ }
1039
+ async function createThreeRuntimeSceneInstance(input, options = {}) {
1040
+ const engineScene = typeof input === "string" ? parseRuntimeScene(input) : parseRuntimeScene(JSON.stringify(input));
1041
+ const root = new Group2();
1042
+ const nodesById = /* @__PURE__ */ new Map();
1043
+ const lights = [];
1044
+ const physicsDescriptors = [];
1045
+ const runtimeNodesById = new Map(engineScene.nodes.map((node) => [node.id, node]));
1046
+ const sceneGraph = resolveSceneGraph(engineScene.nodes, engineScene.entities);
1047
+ const objectFactory = createWebHammerSceneObjectFactory(engineScene, options);
1048
+ const createdObjects = await Promise.all(
1049
+ engineScene.nodes.map(async (node) => [node.id, await objectFactory.createNodeObject(node)])
1050
+ );
1051
+ const attachedNodeIds = /* @__PURE__ */ new Set();
1052
+ const attachStack = /* @__PURE__ */ new Set();
1053
+ root.name = "Web Hammer Scene";
1054
+ root.userData.webHammer = {
1055
+ metadata: engineScene.metadata,
1056
+ settings: engineScene.settings
1057
+ };
1058
+ if (options.applyToScene) {
1059
+ await applyWebHammerWorldSettings(options.applyToScene, engineScene, options);
1060
+ }
1061
+ const worldAmbient = createWorldAmbientLight(engineScene);
1062
+ if (worldAmbient) {
1063
+ root.add(worldAmbient);
1064
+ lights.push(worldAmbient);
1065
+ }
1066
+ createdObjects.forEach(([nodeId, object]) => {
1067
+ nodesById.set(nodeId, object);
1068
+ });
1069
+ const attachNode = (nodeId) => {
1070
+ if (attachedNodeIds.has(nodeId)) {
1071
+ return;
1072
+ }
1073
+ const node = runtimeNodesById.get(nodeId);
1074
+ const object = nodesById.get(nodeId);
1075
+ if (!node || !object) {
1076
+ return;
1077
+ }
1078
+ if (attachStack.has(nodeId)) {
1079
+ root.add(object);
1080
+ attachedNodeIds.add(nodeId);
1081
+ return;
1082
+ }
1083
+ attachStack.add(nodeId);
1084
+ const parentObject = node.parentId && node.parentId !== node.id ? nodesById.get(node.parentId) : void 0;
1085
+ if (parentObject && !attachStack.has(node.parentId)) {
1086
+ attachNode(node.parentId);
1087
+ parentObject.add(object);
1088
+ } else {
1089
+ root.add(object);
1090
+ }
1091
+ attachStack.delete(nodeId);
1092
+ attachedNodeIds.add(nodeId);
1093
+ };
1094
+ for (const node of engineScene.nodes) {
1095
+ attachNode(node.id);
1096
+ }
1097
+ const instancingObjects = await objectFactory.createInstancingObjects();
1098
+ instancingObjects.forEach((object) => {
1099
+ root.add(object);
1100
+ });
1101
+ for (const node of engineScene.nodes) {
1102
+ const object = nodesById.get(node.id);
1103
+ if (!object) {
1104
+ continue;
1105
+ }
1106
+ const light = findPrimaryLight(object);
1107
+ if (light) {
1108
+ lights.push(light);
1109
+ }
1110
+ const physics = extractPhysics(node);
1111
+ if (physics?.enabled) {
1112
+ physicsDescriptors.push({
1113
+ nodeId: node.id,
1114
+ object,
1115
+ physics
1116
+ });
1117
+ }
1118
+ }
1119
+ return {
1120
+ dispose() {
1121
+ disposeThreeRuntimeSceneInstance(root);
1122
+ },
1123
+ entities: engineScene.entities.map((entity) => ({
1124
+ ...entity,
1125
+ transform: sceneGraph.entityWorldTransforms.get(entity.id) ?? entity.transform
1126
+ })),
1127
+ lights,
1128
+ nodesById,
1129
+ physicsDescriptors,
1130
+ root,
1131
+ scene: engineScene
1132
+ };
1133
+ }
1134
+ async function loadWebHammerEngineScene(input, options = {}) {
1135
+ const instance = await createThreeRuntimeSceneInstance(input, options);
1136
+ return {
1137
+ ...instance,
1138
+ nodes: instance.nodesById,
1139
+ physicsNodes: instance.physicsDescriptors
1140
+ };
1141
+ }
1142
+ async function loadWebHammerEngineSceneFromUrl(url, options = {}) {
1143
+ const scene = await fetchWebHammerEngineScene(url);
1144
+ return loadWebHammerEngineScene(scene, options);
1145
+ }
1146
+ async function loadThreeRuntimeSceneInstanceFromUrl(url, options = {}) {
1147
+ const scene = await fetchWebHammerEngineScene(url);
1148
+ return createThreeRuntimeSceneInstance(scene, options);
1149
+ }
1150
+ var APPLIED_WORLD_SETTINGS_KEY = "__webHammerWorldSettings";
1151
+ async function applyWebHammerWorldSettings(target, engineScene, options = {}) {
1152
+ const state = getAppliedWorldSettingsState(target);
1153
+ state.requestId += 1;
1154
+ disposeAppliedSkybox(target, state);
1155
+ const { fogColor, fogFar, fogNear } = engineScene.settings.world;
1156
+ target.fog = fogFar > fogNear ? new Fog(new Color(fogColor), fogNear, fogFar) : null;
1157
+ const skybox = engineScene.settings.world.skybox;
1158
+ if (!skybox.enabled || !skybox.source) {
1159
+ return;
1160
+ }
1161
+ const requestId = state.requestId;
1162
+ try {
1163
+ const resolvedPath = options.resolveAssetUrl ? await options.resolveAssetUrl({
1164
+ kind: "skybox",
1165
+ path: skybox.source,
1166
+ skybox
1167
+ }) : skybox.source;
1168
+ const texture = await loadSkyboxTexture(resolvedPath, skybox);
1169
+ if (getAppliedWorldSettingsState(target).requestId !== requestId) {
1170
+ texture.dispose();
1171
+ return;
1172
+ }
1173
+ target.background = texture;
1174
+ target.backgroundBlurriness = skybox.blur;
1175
+ target.backgroundIntensity = skybox.intensity;
1176
+ target.environment = skybox.affectsLighting ? texture : null;
1177
+ target.environmentIntensity = skybox.affectsLighting ? skybox.lightingIntensity : 1;
1178
+ state.skyboxTexture = texture;
1179
+ } catch {
1180
+ if (getAppliedWorldSettingsState(target).requestId === requestId) {
1181
+ disposeAppliedSkybox(target, state);
1182
+ }
1183
+ }
1184
+ }
1185
+ function clearWebHammerWorldSettings(target) {
1186
+ const state = getAppliedWorldSettingsState(target);
1187
+ state.requestId += 1;
1188
+ disposeAppliedSkybox(target, state);
1189
+ target.fog = null;
1190
+ }
1191
+ var applyRuntimeWorldSettingsToThreeScene = applyWebHammerWorldSettings;
1192
+ var clearRuntimeWorldSettingsFromThreeScene = clearWebHammerWorldSettings;
1193
+ function createWorldAmbientLight(engineScene) {
1194
+ const { ambientColor, ambientIntensity } = engineScene.settings.world;
1195
+ if (ambientIntensity <= 0) {
1196
+ return void 0;
1197
+ }
1198
+ const light = new AmbientLight2(ambientColor, ambientIntensity);
1199
+ light.name = "World Ambient";
1200
+ light.userData.webHammer = {
1201
+ source: "world-settings"
1202
+ };
1203
+ return light;
1204
+ }
1205
+ function getAppliedWorldSettingsState(target) {
1206
+ const userData = target.userData;
1207
+ const existing = userData[APPLIED_WORLD_SETTINGS_KEY];
1208
+ if (existing) {
1209
+ return existing;
1210
+ }
1211
+ const created = {
1212
+ requestId: 0
1213
+ };
1214
+ userData[APPLIED_WORLD_SETTINGS_KEY] = created;
1215
+ return created;
1216
+ }
1217
+ function disposeAppliedSkybox(target, state) {
1218
+ if (state.skyboxTexture) {
1219
+ if (target.background === state.skyboxTexture) {
1220
+ target.background = null;
1221
+ }
1222
+ if (target.environment === state.skyboxTexture) {
1223
+ target.environment = null;
1224
+ }
1225
+ state.skyboxTexture.dispose();
1226
+ state.skyboxTexture = void 0;
1227
+ }
1228
+ target.backgroundBlurriness = 0;
1229
+ target.backgroundIntensity = 1;
1230
+ target.environmentIntensity = 1;
1231
+ }
1232
+ function disposeThreeRuntimeSceneInstance(root) {
1233
+ const geometries = /* @__PURE__ */ new Set();
1234
+ const materials = /* @__PURE__ */ new Set();
1235
+ const textures = /* @__PURE__ */ new Set();
1236
+ root.traverse((object) => {
1237
+ if (!(object instanceof Mesh2 || object instanceof InstancedMesh2)) {
1238
+ return;
1239
+ }
1240
+ if (object.geometry instanceof BufferGeometry2) {
1241
+ geometries.add(object.geometry);
1242
+ }
1243
+ const objectMaterials = Array.isArray(object.material) ? object.material : [object.material];
1244
+ objectMaterials.forEach((material) => {
1245
+ if (!(material instanceof MeshStandardMaterial2)) {
1246
+ return;
1247
+ }
1248
+ materials.add(material);
1249
+ if (material.map) {
1250
+ textures.add(material.map);
1251
+ }
1252
+ if (material.normalMap) {
1253
+ textures.add(material.normalMap);
1254
+ }
1255
+ if (material.metalnessMap) {
1256
+ textures.add(material.metalnessMap);
1257
+ }
1258
+ if (material.roughnessMap) {
1259
+ textures.add(material.roughnessMap);
1260
+ }
1261
+ });
1262
+ });
1263
+ root.removeFromParent();
1264
+ geometries.forEach((geometry) => {
1265
+ geometry.dispose();
1266
+ });
1267
+ materials.forEach((material) => {
1268
+ material.dispose();
1269
+ });
1270
+ textures.forEach((texture) => {
1271
+ texture.dispose();
1272
+ });
1273
+ }
1274
+ async function loadSkyboxTexture(path, skybox) {
1275
+ const texture = skybox.format === "hdr" ? await hdrLoader.loadAsync(path) : await textureLoader2.loadAsync(path);
1276
+ texture.mapping = EquirectangularReflectionMapping;
1277
+ if (skybox.format === "image") {
1278
+ texture.colorSpace = SRGBColorSpace2;
1279
+ }
1280
+ return texture;
1281
+ }
1282
+
1283
+ // ../runtime-build/src/bundle.ts
1284
+ import { unzipSync, zipSync } from "fflate";
1285
+ var TEXTURE_FIELDS = ["baseColorTexture", "metallicRoughnessTexture", "normalTexture"];
1286
+ async function externalizeRuntimeAssets(scene, options = {}) {
1287
+ const manifest = structuredClone(scene);
1288
+ const files = [];
1289
+ const assetDir = trimSlashes(options.assetDir ?? "assets");
1290
+ const copyExternalAssets = options.copyExternalAssets ?? true;
1291
+ const pathBySource = /* @__PURE__ */ new Map();
1292
+ const usedPaths = /* @__PURE__ */ new Set();
1293
+ for (const material of manifest.materials) {
1294
+ for (const field of TEXTURE_FIELDS) {
1295
+ const source = material[field];
1296
+ if (!source) {
1297
+ continue;
1298
+ }
1299
+ const bundledPath = await materializeSource(source, {
1300
+ copyExternalAssets,
1301
+ files,
1302
+ pathBySource,
1303
+ preferredStem: `${assetDir}/textures/${slugify(material.id)}-${textureFieldSuffix(field)}`,
1304
+ usedPaths
1305
+ });
1306
+ if (bundledPath) {
1307
+ material[field] = bundledPath;
1308
+ }
1309
+ }
1310
+ }
1311
+ for (const asset of manifest.assets) {
1312
+ if (asset.type !== "model") {
1313
+ continue;
1314
+ }
1315
+ const bundledPath = await materializeSource(asset.path, {
1316
+ copyExternalAssets,
1317
+ files,
1318
+ pathBySource,
1319
+ preferredExtension: inferModelExtension(asset.path, asset.metadata.modelFormat),
1320
+ preferredStem: `${assetDir}/models/${slugify(asset.id)}`,
1321
+ usedPaths
1322
+ });
1323
+ if (bundledPath) {
1324
+ asset.path = bundledPath;
1325
+ }
1326
+ const texturePath = asset.metadata.texturePath;
1327
+ if (typeof texturePath === "string" && texturePath.length > 0) {
1328
+ const bundledTexturePath = await materializeSource(texturePath, {
1329
+ copyExternalAssets,
1330
+ files,
1331
+ pathBySource,
1332
+ preferredStem: `${assetDir}/model-textures/${slugify(asset.id)}`,
1333
+ usedPaths
1334
+ });
1335
+ if (bundledTexturePath) {
1336
+ asset.metadata.texturePath = bundledTexturePath;
1337
+ }
1338
+ }
1339
+ }
1340
+ const skyboxSource = manifest.settings.world.skybox.source;
1341
+ if (skyboxSource) {
1342
+ const bundledSkyboxPath = await materializeSource(skyboxSource, {
1343
+ copyExternalAssets,
1344
+ files,
1345
+ pathBySource,
1346
+ preferredExtension: manifest.settings.world.skybox.format === "hdr" ? "hdr" : inferExtensionFromPath(skyboxSource),
1347
+ preferredStem: `${assetDir}/skyboxes/${slugify(manifest.settings.world.skybox.name || "skybox")}`,
1348
+ usedPaths
1349
+ });
1350
+ if (bundledSkyboxPath) {
1351
+ manifest.settings.world.skybox.source = bundledSkyboxPath;
1352
+ }
1353
+ }
1354
+ return {
1355
+ files,
1356
+ manifest
1357
+ };
1358
+ }
1359
+ async function buildRuntimeBundle(scene, options = {}) {
1360
+ return externalizeRuntimeAssets(scene, options);
1361
+ }
1362
+ function normalizeRuntimeScene(scene) {
1363
+ return typeof scene === "string" ? parseRuntimeScene(scene) : parseRuntimeScene(JSON.stringify(scene));
1364
+ }
1365
+ function packRuntimeBundle(bundle, options = {}) {
1366
+ const manifestPath = options.manifestPath ?? "scene.runtime.json";
1367
+ const encoder = new TextEncoder();
1368
+ const entries = {
1369
+ [manifestPath]: encoder.encode(JSON.stringify(bundle.manifest))
1370
+ };
1371
+ bundle.files.forEach((file) => {
1372
+ entries[file.path] = file.bytes;
1373
+ });
1374
+ return zipSync(entries, {
1375
+ level: options.compressionLevel ?? 6
1376
+ });
1377
+ }
1378
+ function unpackRuntimeBundle(bytes, options = {}) {
1379
+ const manifestPath = options.manifestPath ?? "scene.runtime.json";
1380
+ const archive = unzipSync(bytes);
1381
+ const manifestBytes = archive[manifestPath];
1382
+ if (!manifestBytes) {
1383
+ throw new Error(`Bundle is missing ${manifestPath}.`);
1384
+ }
1385
+ const manifest = parseRuntimeScene(new TextDecoder().decode(manifestBytes));
1386
+ const files = Object.entries(archive).filter(([path]) => path !== manifestPath).map(([path, fileBytes]) => ({
1387
+ bytes: fileBytes,
1388
+ mimeType: inferMimeTypeFromPath(path),
1389
+ path
1390
+ }));
1391
+ return {
1392
+ files,
1393
+ manifest
1394
+ };
1395
+ }
1396
+ function buildRuntimeWorldIndex(chunks, options = {}) {
1397
+ return {
1398
+ chunks,
1399
+ sharedAssets: options.sharedAssets,
1400
+ version: options.version ?? 1
1401
+ };
1402
+ }
1403
+ async function externalizeWebHammerEngineScene(scene, options = {}) {
1404
+ return externalizeRuntimeAssets(scene, options);
1405
+ }
1406
+ function createWebHammerEngineBundleZip(bundle, options = {}) {
1407
+ return packRuntimeBundle(bundle, options);
1408
+ }
1409
+ function parseWebHammerEngineBundleZip(bytes, options = {}) {
1410
+ return unpackRuntimeBundle(bytes, options);
1411
+ }
1412
+ async function materializeSource(source, context) {
1413
+ const existing = context.pathBySource.get(source);
1414
+ if (existing) {
1415
+ return existing;
1416
+ }
1417
+ if (isDataUrl(source)) {
1418
+ const parsed = parseDataUrl(source);
1419
+ const path2 = ensureUniquePath(
1420
+ `${context.preferredStem}.${inferExtension(parsed.mimeType, context.preferredExtension)}`,
1421
+ context.usedPaths
1422
+ );
1423
+ context.files.push({
1424
+ bytes: parsed.bytes,
1425
+ mimeType: parsed.mimeType,
1426
+ path: path2
1427
+ });
1428
+ context.pathBySource.set(source, path2);
1429
+ return path2;
1430
+ }
1431
+ if (!context.copyExternalAssets) {
1432
+ return void 0;
1433
+ }
1434
+ const response = await fetch(source);
1435
+ if (!response.ok) {
1436
+ throw new Error(`Failed to bundle asset: ${source}`);
1437
+ }
1438
+ const blob = await response.blob();
1439
+ const bytes = new Uint8Array(await blob.arrayBuffer());
1440
+ const path = ensureUniquePath(
1441
+ `${context.preferredStem}.${inferExtension(blob.type, context.preferredExtension ?? inferExtensionFromPath(source))}`,
1442
+ context.usedPaths
1443
+ );
1444
+ context.files.push({
1445
+ bytes,
1446
+ mimeType: blob.type || "application/octet-stream",
1447
+ path
1448
+ });
1449
+ context.pathBySource.set(source, path);
1450
+ return path;
1451
+ }
1452
+ function parseDataUrl(source) {
1453
+ const match = /^data:([^;,]+)?(?:;charset=[^;,]+)?(;base64)?,(.*)$/i.exec(source);
1454
+ if (!match) {
1455
+ throw new Error("Invalid data URL.");
1456
+ }
1457
+ const mimeType = match[1] || "application/octet-stream";
1458
+ const payload = match[3] || "";
1459
+ if (match[2]) {
1460
+ const binary = atob(payload);
1461
+ const bytes = new Uint8Array(binary.length);
1462
+ for (let index = 0; index < binary.length; index += 1) {
1463
+ bytes[index] = binary.charCodeAt(index);
1464
+ }
1465
+ return { bytes, mimeType };
1466
+ }
1467
+ return {
1468
+ bytes: new TextEncoder().encode(decodeURIComponent(payload)),
1469
+ mimeType
1470
+ };
1471
+ }
1472
+ function textureFieldSuffix(field) {
1473
+ switch (field) {
1474
+ case "baseColorTexture":
1475
+ return "color";
1476
+ case "metallicRoughnessTexture":
1477
+ return "orm";
1478
+ default:
1479
+ return "normal";
1480
+ }
1481
+ }
1482
+ function inferModelExtension(path, modelFormat) {
1483
+ if (typeof modelFormat === "string" && modelFormat.length > 0) {
1484
+ return modelFormat.toLowerCase();
1485
+ }
1486
+ return inferExtensionFromPath(path) ?? "bin";
1487
+ }
1488
+ function inferExtension(mimeType, fallback) {
1489
+ const normalized = mimeType?.toLowerCase();
1490
+ if (normalized === "image/png") {
1491
+ return "png";
1492
+ }
1493
+ if (normalized === "image/jpeg") {
1494
+ return "jpg";
1495
+ }
1496
+ if (normalized === "image/svg+xml") {
1497
+ return "svg";
1498
+ }
1499
+ if (normalized === "image/vnd.radiance") {
1500
+ return "hdr";
1501
+ }
1502
+ if (normalized === "model/gltf+json") {
1503
+ return "gltf";
1504
+ }
1505
+ if (normalized === "model/gltf-binary" || normalized === "application/octet-stream") {
1506
+ return fallback ?? "bin";
1507
+ }
1508
+ return fallback ?? "bin";
1509
+ }
1510
+ function inferExtensionFromPath(path) {
1511
+ const cleanPath = path.split("?")[0]?.split("#")[0] ?? path;
1512
+ const parts = cleanPath.split(".");
1513
+ return parts.length > 1 ? parts.at(-1)?.toLowerCase() : void 0;
1514
+ }
1515
+ function inferMimeTypeFromPath(path) {
1516
+ switch (inferExtensionFromPath(path)) {
1517
+ case "png":
1518
+ return "image/png";
1519
+ case "jpg":
1520
+ case "jpeg":
1521
+ return "image/jpeg";
1522
+ case "svg":
1523
+ return "image/svg+xml";
1524
+ case "hdr":
1525
+ return "image/vnd.radiance";
1526
+ case "glb":
1527
+ return "model/gltf-binary";
1528
+ case "gltf":
1529
+ return "model/gltf+json";
1530
+ case "obj":
1531
+ return "text/plain";
1532
+ case "mtl":
1533
+ return "text/plain";
1534
+ case "json":
1535
+ return "application/json";
1536
+ default:
1537
+ return "application/octet-stream";
1538
+ }
1539
+ }
1540
+ function ensureUniquePath(path, usedPaths) {
1541
+ if (!usedPaths.has(path)) {
1542
+ usedPaths.add(path);
1543
+ return path;
1544
+ }
1545
+ const lastDot = path.lastIndexOf(".");
1546
+ const stem = lastDot >= 0 ? path.slice(0, lastDot) : path;
1547
+ const extension = lastDot >= 0 ? path.slice(lastDot) : "";
1548
+ let counter = 2;
1549
+ while (usedPaths.has(`${stem}-${counter}${extension}`)) {
1550
+ counter += 1;
1551
+ }
1552
+ const resolved = `${stem}-${counter}${extension}`;
1553
+ usedPaths.add(resolved);
1554
+ return resolved;
1555
+ }
1556
+ function isDataUrl(value) {
1557
+ return value.startsWith("data:");
1558
+ }
1559
+ function slugify(value) {
1560
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1561
+ return normalized || "asset";
1562
+ }
1563
+ function trimSlashes(value) {
1564
+ return value.replace(/^\/+|\/+$/g, "");
1565
+ }
1566
+
1567
+ // ../geometry-kernel/src/polygon/polygon-utils.ts
1568
+ import earcut from "earcut";
1569
+ function computePolygonNormal(vertices) {
1570
+ if (vertices.length < 3) {
1571
+ return vec3(0, 1, 0);
1572
+ }
1573
+ let normal = vec3(0, 0, 0);
1574
+ for (let index = 0; index < vertices.length; index += 1) {
1575
+ const current = vertices[index];
1576
+ const next = vertices[(index + 1) % vertices.length];
1577
+ normal = addVec3(
1578
+ normal,
1579
+ vec3(
1580
+ (current.y - next.y) * (current.z + next.z),
1581
+ (current.z - next.z) * (current.x + next.x),
1582
+ (current.x - next.x) * (current.y + next.y)
1583
+ )
1584
+ );
1585
+ }
1586
+ const normalized = normalizeVec3(normal);
1587
+ return lengthVec3(normalized) === 0 ? vec3(0, 1, 0) : normalized;
1588
+ }
1589
+ function projectPolygonToPlane(vertices, normal = computePolygonNormal(vertices)) {
1590
+ const origin = averageVec3(vertices);
1591
+ const tangentReference = Math.abs(normal.y) < 0.99 ? vec3(0, 1, 0) : vec3(1, 0, 0);
1592
+ let tangent = normalizeVec3(crossVec3(tangentReference, normal));
1593
+ if (lengthVec3(tangent) === 0) {
1594
+ tangent = normalizeVec3(crossVec3(vec3(0, 0, 1), normal));
1595
+ }
1596
+ const bitangent = normalizeVec3(crossVec3(normal, tangent));
1597
+ return vertices.map((vertex) => {
1598
+ const offset = subVec3(vertex, origin);
1599
+ return [dotVec3(offset, tangent), dotVec3(offset, bitangent)];
1600
+ });
1601
+ }
1602
+ function polygonSignedArea(points) {
1603
+ let area = 0;
1604
+ for (let index = 0; index < points.length; index += 1) {
1605
+ const [x1, y1] = points[index];
1606
+ const [x2, y2] = points[(index + 1) % points.length];
1607
+ area += x1 * y2 - x2 * y1;
1608
+ }
1609
+ return area * 0.5;
1610
+ }
1611
+ function sortVerticesOnPlane(vertices, normal) {
1612
+ const center = averageVec3(vertices);
1613
+ const tangentReference = Math.abs(normal.y) < 0.99 ? vec3(0, 1, 0) : vec3(1, 0, 0);
1614
+ let tangent = normalizeVec3(crossVec3(tangentReference, normal));
1615
+ if (lengthVec3(tangent) === 0) {
1616
+ tangent = normalizeVec3(crossVec3(vec3(0, 0, 1), normal));
1617
+ }
1618
+ const bitangent = normalizeVec3(crossVec3(normal, tangent));
1619
+ const sorted = [...vertices].sort((left, right) => {
1620
+ const leftOffset = subVec3(left, center);
1621
+ const rightOffset = subVec3(right, center);
1622
+ const leftAngle = Math.atan2(dotVec3(leftOffset, bitangent), dotVec3(leftOffset, tangent));
1623
+ const rightAngle = Math.atan2(dotVec3(rightOffset, bitangent), dotVec3(rightOffset, tangent));
1624
+ return leftAngle - rightAngle;
1625
+ });
1626
+ if (sorted.length < 3) {
1627
+ return sorted;
1628
+ }
1629
+ const windingNormal = normalizeVec3(
1630
+ crossVec3(subVec3(sorted[1], sorted[0]), subVec3(sorted[2], sorted[0]))
1631
+ );
1632
+ return dotVec3(windingNormal, normal) < 0 ? sorted.reverse() : sorted;
1633
+ }
1634
+ function triangulatePolygon(points) {
1635
+ const flattened = points.flatMap(([x, y]) => [x, y]);
1636
+ return earcut(flattened);
1637
+ }
1638
+ function triangulatePolygon3D(vertices, normal = computePolygonNormal(vertices)) {
1639
+ if (vertices.length < 3) {
1640
+ return [];
1641
+ }
1642
+ const projected = projectPolygonToPlane(vertices, normal);
1643
+ if (Math.abs(polygonSignedArea(projected)) <= 1e-6) {
1644
+ return [];
1645
+ }
1646
+ return triangulatePolygon(projected);
1647
+ }
1648
+ function computeFaceCenter(vertices) {
1649
+ return averageVec3(vertices);
1650
+ }
1651
+
1652
+ // ../geometry-kernel/src/brush/brush-kernel.ts
1653
+ function reconstructBrushFaces(brush, epsilon = 1e-4) {
1654
+ if (brush.planes.length < 4) {
1655
+ return {
1656
+ faces: [],
1657
+ vertices: [],
1658
+ valid: false,
1659
+ errors: ["Brush reconstruction requires at least four planes."]
1660
+ };
1661
+ }
1662
+ const vertexRegistry = /* @__PURE__ */ new Map();
1663
+ const faces = [];
1664
+ for (let planeIndex = 0; planeIndex < brush.planes.length; planeIndex += 1) {
1665
+ const plane = brush.planes[planeIndex];
1666
+ const faceVertices = collectFaceVertices(brush.planes, planeIndex, epsilon);
1667
+ if (faceVertices.length < 3) {
1668
+ continue;
1669
+ }
1670
+ const orderedVertices = sortVerticesOnPlane(faceVertices, normalizeVec3(plane.normal));
1671
+ const triangleIndices = triangulatePolygon3D(orderedVertices, plane.normal);
1672
+ if (triangleIndices.length < 3) {
1673
+ continue;
1674
+ }
1675
+ const vertices = orderedVertices.map((position) => registerBrushVertex(vertexRegistry, position, epsilon));
1676
+ const seedFace = brush.faces[planeIndex];
1677
+ faces.push({
1678
+ id: seedFace?.id ?? `face:brush:${planeIndex}`,
1679
+ plane,
1680
+ materialId: seedFace?.materialId,
1681
+ uvOffset: seedFace?.uvOffset,
1682
+ uvScale: seedFace?.uvScale,
1683
+ vertexIds: vertices.map((vertex) => vertex.id),
1684
+ vertices,
1685
+ center: computeFaceCenter(orderedVertices),
1686
+ normal: normalizeVec3(plane.normal),
1687
+ triangleIndices
1688
+ });
1689
+ }
1690
+ return {
1691
+ faces,
1692
+ vertices: Array.from(vertexRegistry.values()),
1693
+ valid: faces.length >= 4,
1694
+ errors: faces.length >= 4 ? [] : ["Brush reconstruction did not produce a closed convex solid."]
1695
+ };
1696
+ }
1697
+ function classifyPointAgainstPlane(point, plane, epsilon = 1e-4) {
1698
+ const signedDistance = signedDistanceToPlane(point, plane);
1699
+ return signedDistance > epsilon ? "outside" : "inside";
1700
+ }
1701
+ function signedDistanceToPlane(point, plane) {
1702
+ return dotVec3(plane.normal, point) - plane.distance;
1703
+ }
1704
+ function intersectPlanes(first, second, third, epsilon = 1e-6) {
1705
+ const denominator = dotVec3(first.normal, crossVec3(second.normal, third.normal));
1706
+ if (Math.abs(denominator) <= epsilon) {
1707
+ return void 0;
1708
+ }
1709
+ const firstTerm = scaleVec3(crossVec3(second.normal, third.normal), first.distance);
1710
+ const secondTerm = scaleVec3(crossVec3(third.normal, first.normal), second.distance);
1711
+ const thirdTerm = scaleVec3(crossVec3(first.normal, second.normal), third.distance);
1712
+ return scaleVec3(addVec3(addVec3(firstTerm, secondTerm), thirdTerm), 1 / denominator);
1713
+ }
1714
+ function collectFaceVertices(planes, planeIndex, epsilon) {
1715
+ const plane = planes[planeIndex];
1716
+ const vertices = /* @__PURE__ */ new Map();
1717
+ for (let firstIndex = 0; firstIndex < planes.length; firstIndex += 1) {
1718
+ if (firstIndex === planeIndex) {
1719
+ continue;
1720
+ }
1721
+ for (let secondIndex = firstIndex + 1; secondIndex < planes.length; secondIndex += 1) {
1722
+ if (secondIndex === planeIndex) {
1723
+ continue;
1724
+ }
1725
+ const intersection = intersectPlanes(plane, planes[firstIndex], planes[secondIndex], epsilon);
1726
+ if (!intersection) {
1727
+ continue;
1728
+ }
1729
+ const liesOnPlane = Math.abs(signedDistanceToPlane(intersection, plane)) <= epsilon * 4;
1730
+ const insideAllPlanes = planes.every(
1731
+ (candidatePlane) => classifyPointAgainstPlane(intersection, candidatePlane, epsilon * 4) === "inside"
1732
+ );
1733
+ if (!liesOnPlane || !insideAllPlanes) {
1734
+ continue;
1735
+ }
1736
+ vertices.set(makeVertexKey(intersection, epsilon), intersection);
1737
+ }
1738
+ }
1739
+ return Array.from(vertices.values());
1740
+ }
1741
+ function registerBrushVertex(registry, position, epsilon) {
1742
+ const key = makeVertexKey(position, epsilon);
1743
+ const existing = registry.get(key);
1744
+ if (existing) {
1745
+ return existing;
1746
+ }
1747
+ const vertex = {
1748
+ id: `vertex:brush:${registry.size}`,
1749
+ position: vec3(position.x, position.y, position.z)
1750
+ };
1751
+ registry.set(key, vertex);
1752
+ return vertex;
1753
+ }
1754
+ function makeVertexKey(position, epsilon) {
1755
+ return [
1756
+ Math.round(position.x / epsilon),
1757
+ Math.round(position.y / epsilon),
1758
+ Math.round(position.z / epsilon)
1759
+ ].join(":");
1760
+ }
1761
+
1762
+ // ../geometry-kernel/src/mesh/editable-mesh.ts
1763
+ var editableMeshIndexCache = /* @__PURE__ */ new WeakMap();
1764
+ function getFaceVertexIds(mesh, faceId) {
1765
+ const index = getEditableMeshIndex(mesh);
1766
+ const cachedIds = index.faceVertexIds.get(faceId);
1767
+ if (cachedIds) {
1768
+ return cachedIds;
1769
+ }
1770
+ const face = index.faceById.get(faceId);
1771
+ if (!face) {
1772
+ return [];
1773
+ }
1774
+ const ids = [];
1775
+ let currentEdgeId = face.halfEdge;
1776
+ let guard = 0;
1777
+ while (currentEdgeId && guard < mesh.halfEdges.length + 1) {
1778
+ const halfEdge = index.halfEdgeById.get(currentEdgeId);
1779
+ if (!halfEdge) {
1780
+ return [];
1781
+ }
1782
+ ids.push(halfEdge.vertex);
1783
+ currentEdgeId = halfEdge.next;
1784
+ guard += 1;
1785
+ if (currentEdgeId === face.halfEdge) {
1786
+ break;
1787
+ }
1788
+ }
1789
+ index.faceVertexIds.set(faceId, ids);
1790
+ return ids;
1791
+ }
1792
+ function getFaceVertices(mesh, faceId) {
1793
+ const index = getEditableMeshIndex(mesh);
1794
+ return getFaceVertexIds(mesh, faceId).map((vertexId) => index.vertexById.get(vertexId)).filter((vertex) => Boolean(vertex));
1795
+ }
1796
+ function triangulateMeshFace(mesh, faceId) {
1797
+ const faceVertices = getFaceVertices(mesh, faceId);
1798
+ if (faceVertices.length < 3) {
1799
+ return void 0;
1800
+ }
1801
+ const normal = computePolygonNormal(faceVertices.map((vertex) => vertex.position));
1802
+ const indices = triangulatePolygon3D(
1803
+ faceVertices.map((vertex) => vertex.position),
1804
+ normal
1805
+ );
1806
+ if (indices.length < 3) {
1807
+ return void 0;
1808
+ }
1809
+ return {
1810
+ faceId,
1811
+ vertexIds: faceVertices.map((vertex) => vertex.id),
1812
+ normal,
1813
+ indices
1814
+ };
1815
+ }
1816
+ function getEditableMeshIndex(mesh) {
1817
+ const cached = editableMeshIndexCache.get(mesh);
1818
+ if (cached && cached.faces === mesh.faces && cached.halfEdges === mesh.halfEdges && cached.vertices === mesh.vertices) {
1819
+ return cached;
1820
+ }
1821
+ const nextIndex = {
1822
+ faceById: new Map(mesh.faces.map((face) => [face.id, face])),
1823
+ faceVertexIds: /* @__PURE__ */ new Map(),
1824
+ faces: mesh.faces,
1825
+ halfEdgeById: new Map(mesh.halfEdges.map((halfEdge) => [halfEdge.id, halfEdge])),
1826
+ halfEdges: mesh.halfEdges,
1827
+ vertexById: new Map(mesh.vertices.map((vertex) => [vertex.id, vertex])),
1828
+ vertices: mesh.vertices
1829
+ };
1830
+ editableMeshIndexCache.set(mesh, nextIndex);
1831
+ return nextIndex;
1832
+ }
1833
+
1834
+ // ../runtime-build/src/snapshot-build.ts
1835
+ import { MeshBVH } from "three-mesh-bvh";
1836
+ import {
1837
+ Box3 as Box33,
1838
+ BoxGeometry as BoxGeometry3,
1839
+ BufferGeometry as BufferGeometry3,
1840
+ ConeGeometry,
1841
+ Float32BufferAttribute as Float32BufferAttribute3,
1842
+ Group as Group3,
1843
+ Mesh as Mesh3,
1844
+ MeshStandardMaterial as MeshStandardMaterial3,
1845
+ RepeatWrapping as RepeatWrapping3,
1846
+ Scene as Scene2,
1847
+ SphereGeometry,
1848
+ SRGBColorSpace as SRGBColorSpace3,
1849
+ TextureLoader as TextureLoader3,
1850
+ Vector3 as Vector34,
1851
+ CylinderGeometry
1852
+ } from "three";
1853
+ import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
1854
+ import { GLTFLoader as GLTFLoader3 } from "three/examples/jsm/loaders/GLTFLoader.js";
1855
+ import { MTLLoader as MTLLoader3 } from "three/examples/jsm/loaders/MTLLoader.js";
1856
+ import { OBJLoader as OBJLoader3 } from "three/examples/jsm/loaders/OBJLoader.js";
1857
+ var gltfLoader3 = new GLTFLoader3();
1858
+ var gltfExporter = new GLTFExporter();
1859
+ var mtlLoader3 = new MTLLoader3();
1860
+ var modelTextureLoader2 = new TextureLoader3();
1861
+ async function buildRuntimeScene(input) {
1862
+ if (typeof input === "string") {
1863
+ return parseRuntimeScene(input);
1864
+ }
1865
+ if (isSceneDocumentSnapshotLike(input)) {
1866
+ return buildRuntimeSceneFromSnapshot(input);
1867
+ }
1868
+ return normalizeRuntimeScene(input);
1869
+ }
1870
+ async function buildRuntimeSceneFromSnapshot(snapshot) {
1871
+ const assetsById = new Map(snapshot.assets.map((asset) => [asset.id, asset]));
1872
+ const materialsById = new Map(snapshot.materials.map((material) => [material.id, material]));
1873
+ const exportedMaterials = await Promise.all(snapshot.materials.map((material) => resolveRuntimeMaterial(material)));
1874
+ const shouldBakeLods = snapshot.settings.world.lod.enabled;
1875
+ const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
1876
+ const exportedSettings = shouldBakeLods ? {
1877
+ ...snapshot.settings,
1878
+ world: {
1879
+ ...snapshot.settings.world,
1880
+ lod: {
1881
+ ...snapshot.settings.world.lod,
1882
+ bakedAt: exportedAt
1883
+ }
1884
+ }
1885
+ } : snapshot.settings;
1886
+ const generatedAssets = [];
1887
+ const exportedNodes = [];
1888
+ for (const node of snapshot.nodes) {
1889
+ if (isGroupNode(node)) {
1890
+ exportedNodes.push({
1891
+ data: node.data,
1892
+ hooks: node.hooks,
1893
+ id: node.id,
1894
+ kind: "group",
1895
+ metadata: node.metadata,
1896
+ name: node.name,
1897
+ parentId: node.parentId,
1898
+ tags: node.tags,
1899
+ transform: node.transform
1900
+ });
1901
+ continue;
1902
+ }
1903
+ if (isBrushNode(node)) {
1904
+ const geometry = await buildExportGeometry(node, materialsById);
1905
+ exportedNodes.push({
1906
+ data: node.data,
1907
+ geometry,
1908
+ hooks: node.hooks,
1909
+ id: node.id,
1910
+ kind: "brush",
1911
+ lods: shouldBakeLods ? await buildGeometryLods(geometry, snapshot.settings.world.lod) : void 0,
1912
+ metadata: node.metadata,
1913
+ name: node.name,
1914
+ parentId: node.parentId,
1915
+ tags: node.tags,
1916
+ transform: node.transform
1917
+ });
1918
+ continue;
1919
+ }
1920
+ if (isMeshNode(node)) {
1921
+ const geometry = await buildExportGeometry(node, materialsById);
1922
+ exportedNodes.push({
1923
+ data: node.data,
1924
+ geometry,
1925
+ hooks: node.hooks,
1926
+ id: node.id,
1927
+ kind: "mesh",
1928
+ lods: shouldBakeLods ? await buildGeometryLods(geometry, snapshot.settings.world.lod) : void 0,
1929
+ metadata: node.metadata,
1930
+ name: node.name,
1931
+ parentId: node.parentId,
1932
+ tags: node.tags,
1933
+ transform: node.transform
1934
+ });
1935
+ continue;
1936
+ }
1937
+ if (isPrimitiveNode(node)) {
1938
+ const geometry = await buildExportGeometry(node, materialsById);
1939
+ exportedNodes.push({
1940
+ data: node.data,
1941
+ geometry,
1942
+ hooks: node.hooks,
1943
+ id: node.id,
1944
+ kind: "primitive",
1945
+ lods: shouldBakeLods ? await buildGeometryLods(geometry, snapshot.settings.world.lod) : void 0,
1946
+ metadata: node.metadata,
1947
+ name: node.name,
1948
+ parentId: node.parentId,
1949
+ tags: node.tags,
1950
+ transform: node.transform
1951
+ });
1952
+ continue;
1953
+ }
1954
+ if (isModelNode(node)) {
1955
+ const modelLodBake = shouldBakeLods ? await buildModelLods(node.name, assetsById.get(node.data.assetId), node.id, snapshot.settings.world.lod) : void 0;
1956
+ generatedAssets.push(...modelLodBake?.assets ?? []);
1957
+ exportedNodes.push({
1958
+ data: node.data,
1959
+ hooks: node.hooks,
1960
+ id: node.id,
1961
+ kind: "model",
1962
+ lods: modelLodBake?.lods,
1963
+ metadata: node.metadata,
1964
+ name: node.name,
1965
+ parentId: node.parentId,
1966
+ tags: node.tags,
1967
+ transform: node.transform
1968
+ });
1969
+ continue;
1970
+ }
1971
+ if (isInstancingNode(node)) {
1972
+ const sourceNode = resolveInstancingSourceNode(snapshot.nodes, node);
1973
+ if (!sourceNode || !(isBrushNode(sourceNode) || isMeshNode(sourceNode) || isPrimitiveNode(sourceNode) || isModelNode(sourceNode))) {
1974
+ continue;
1975
+ }
1976
+ exportedNodes.push({
1977
+ data: {
1978
+ sourceNodeId: sourceNode.id
1979
+ },
1980
+ hooks: node.hooks,
1981
+ id: node.id,
1982
+ kind: "instancing",
1983
+ metadata: node.metadata,
1984
+ name: node.name,
1985
+ parentId: node.parentId,
1986
+ tags: node.tags,
1987
+ transform: sanitizeInstanceTransform(node.transform)
1988
+ });
1989
+ continue;
1990
+ }
1991
+ exportedNodes.push({
1992
+ data: node.data,
1993
+ id: node.id,
1994
+ kind: "light",
1995
+ metadata: node.metadata,
1996
+ name: node.name,
1997
+ parentId: node.parentId,
1998
+ tags: node.tags,
1999
+ transform: node.transform
2000
+ });
2001
+ }
2002
+ return {
2003
+ assets: [...snapshot.assets, ...generatedAssets],
2004
+ entities: snapshot.entities,
2005
+ layers: snapshot.layers,
2006
+ materials: exportedMaterials,
2007
+ metadata: {
2008
+ exportedAt,
2009
+ format: "web-hammer-engine",
2010
+ version: CURRENT_RUNTIME_SCENE_VERSION
2011
+ },
2012
+ nodes: exportedNodes,
2013
+ settings: exportedSettings
2014
+ };
2015
+ }
2016
+ function isSceneDocumentSnapshotLike(value) {
2017
+ return Boolean(
2018
+ value && typeof value === "object" && Array.isArray(value.nodes) && Array.isArray(value.materials) && Array.isArray(value.textures)
2019
+ );
2020
+ }
2021
+ async function buildExportGeometry(node, materialsById) {
2022
+ const fallbackMaterial = await resolveRuntimeMaterial({
2023
+ color: node.kind === "brush" ? "#f69036" : node.kind === "primitive" && node.data.role === "prop" ? "#7f8ea3" : "#6ed5c0",
2024
+ id: `material:fallback:${node.id}`,
2025
+ metalness: node.kind === "brush" ? 0 : node.kind === "primitive" && node.data.role === "prop" ? 0.12 : 0.05,
2026
+ name: `${node.name} Default`,
2027
+ roughness: node.kind === "brush" ? 0.95 : node.kind === "primitive" && node.data.role === "prop" ? 0.64 : 0.82
2028
+ });
2029
+ const primitiveByMaterial = /* @__PURE__ */ new Map();
2030
+ const appendFace = async (params) => {
2031
+ const material = params.faceMaterialId ? await resolveRuntimeMaterial(materialsById.get(params.faceMaterialId)) : fallbackMaterial;
2032
+ const primitive = primitiveByMaterial.get(material.id) ?? {
2033
+ indices: [],
2034
+ material,
2035
+ normals: [],
2036
+ positions: [],
2037
+ uvs: []
2038
+ };
2039
+ const vertexOffset = primitive.positions.length / 3;
2040
+ const uvs = params.uvs && params.uvs.length === params.vertices.length ? params.uvs.flatMap((uv) => [uv.x, uv.y]) : projectPlanarUvs(params.vertices, params.normal, params.uvScale, params.uvOffset);
2041
+ params.vertices.forEach((vertex) => {
2042
+ primitive.positions.push(vertex.x, vertex.y, vertex.z);
2043
+ primitive.normals.push(params.normal.x, params.normal.y, params.normal.z);
2044
+ });
2045
+ primitive.uvs.push(...uvs);
2046
+ params.triangleIndices.forEach((index) => {
2047
+ primitive.indices.push(vertexOffset + index);
2048
+ });
2049
+ primitiveByMaterial.set(material.id, primitive);
2050
+ };
2051
+ if (isBrushNode(node)) {
2052
+ const rebuilt = reconstructBrushFaces(node.data);
2053
+ if (!rebuilt.valid) {
2054
+ return { primitives: [] };
2055
+ }
2056
+ for (const face of rebuilt.faces) {
2057
+ await appendFace({
2058
+ faceMaterialId: face.materialId,
2059
+ normal: face.normal,
2060
+ triangleIndices: face.triangleIndices,
2061
+ uvOffset: face.uvOffset,
2062
+ uvScale: face.uvScale,
2063
+ vertices: face.vertices.map((vertex) => vertex.position)
2064
+ });
2065
+ }
2066
+ }
2067
+ if (isMeshNode(node)) {
2068
+ for (const face of node.data.faces) {
2069
+ const triangulated = triangulateMeshFace(node.data, face.id);
2070
+ if (!triangulated) {
2071
+ continue;
2072
+ }
2073
+ await appendFace({
2074
+ faceMaterialId: face.materialId,
2075
+ normal: triangulated.normal,
2076
+ triangleIndices: triangulated.indices,
2077
+ uvOffset: face.uvOffset,
2078
+ uvScale: face.uvScale,
2079
+ uvs: face.uvs,
2080
+ vertices: getFaceVertices(node.data, face.id).map((vertex) => vertex.position)
2081
+ });
2082
+ }
2083
+ }
2084
+ if (isPrimitiveNode(node)) {
2085
+ const material = node.data.materialId ? await resolveRuntimeMaterial(materialsById.get(node.data.materialId)) : fallbackMaterial;
2086
+ const primitive = buildPrimitiveGeometry(node.data.shape, node.data.size, node.data.radialSegments ?? 24);
2087
+ if (primitive) {
2088
+ primitiveByMaterial.set(material.id, {
2089
+ indices: primitive.indices,
2090
+ material,
2091
+ normals: primitive.normals,
2092
+ positions: primitive.positions,
2093
+ uvs: primitive.uvs
2094
+ });
2095
+ }
2096
+ }
2097
+ return {
2098
+ primitives: Array.from(primitiveByMaterial.values())
2099
+ };
2100
+ }
2101
+ async function buildGeometryLods(geometry, settings) {
2102
+ if (!geometry.primitives.length) {
2103
+ return void 0;
2104
+ }
2105
+ const midGeometry = simplifyExportGeometry(geometry, settings.midDetailRatio);
2106
+ const lowGeometry = simplifyExportGeometry(geometry, settings.lowDetailRatio);
2107
+ const lods = [];
2108
+ if (midGeometry) {
2109
+ lods.push({
2110
+ geometry: midGeometry,
2111
+ level: "mid"
2112
+ });
2113
+ }
2114
+ if (lowGeometry) {
2115
+ lods.push({
2116
+ geometry: lowGeometry,
2117
+ level: "low"
2118
+ });
2119
+ }
2120
+ return lods.length ? lods : void 0;
2121
+ }
2122
+ async function buildModelLods(name, asset, nodeId, settings) {
2123
+ if (!asset?.path) {
2124
+ return { assets: [], lods: void 0 };
2125
+ }
2126
+ const source = await loadModelSceneForLodBake(asset);
2127
+ const bakedLevels = [];
2128
+ for (const [level, ratio] of [
2129
+ ["mid", settings.midDetailRatio],
2130
+ ["low", settings.lowDetailRatio]
2131
+ ]) {
2132
+ const simplified = simplifyModelSceneForRatio(source, ratio);
2133
+ if (!simplified) {
2134
+ continue;
2135
+ }
2136
+ const bytes = await exportModelSceneAsGlb(simplified);
2137
+ bakedLevels.push({
2138
+ asset: createGeneratedModelLodAsset(asset, name, nodeId, level, bytes),
2139
+ level
2140
+ });
2141
+ }
2142
+ return {
2143
+ assets: bakedLevels.map((entry) => entry.asset),
2144
+ lods: bakedLevels.length ? bakedLevels.map((entry) => ({
2145
+ assetId: entry.asset.id,
2146
+ level: entry.level
2147
+ })) : void 0
2148
+ };
2149
+ }
2150
+ async function loadModelSceneForLodBake(asset) {
2151
+ const format = resolveModelAssetFormat(asset);
2152
+ if (format === "obj") {
2153
+ const objLoader = new OBJLoader3();
2154
+ const texturePath = readModelAssetString(asset, "texturePath");
2155
+ const resolvedTexturePath = typeof texturePath === "string" && texturePath.length > 0 ? texturePath : void 0;
2156
+ const mtlText = readModelAssetString(asset, "materialMtlText");
2157
+ if (mtlText) {
2158
+ const materialCreator = mtlLoader3.parse(patchMtlTextureReferences2(mtlText, resolvedTexturePath), "");
2159
+ materialCreator.preload();
2160
+ objLoader.setMaterials(materialCreator);
2161
+ } else {
2162
+ objLoader.setMaterials(void 0);
2163
+ }
2164
+ const object = await objLoader.loadAsync(asset.path);
2165
+ if (!mtlText && resolvedTexturePath) {
2166
+ const texture = await modelTextureLoader2.loadAsync(resolvedTexturePath);
2167
+ texture.wrapS = RepeatWrapping3;
2168
+ texture.wrapT = RepeatWrapping3;
2169
+ texture.colorSpace = SRGBColorSpace3;
2170
+ object.traverse((child) => {
2171
+ if (child instanceof Mesh3) {
2172
+ child.material = new MeshStandardMaterial3({
2173
+ map: texture,
2174
+ metalness: 0.12,
2175
+ roughness: 0.76
2176
+ });
2177
+ }
2178
+ });
2179
+ }
2180
+ return object;
2181
+ }
2182
+ return (await gltfLoader3.loadAsync(asset.path)).scene;
2183
+ }
2184
+ function simplifyModelSceneForRatio(source, ratio) {
2185
+ if (ratio >= 0.98) {
2186
+ return void 0;
2187
+ }
2188
+ const simplifiedRoot = source.clone(true);
2189
+ expandGroupedModelMeshesForLodBake(simplifiedRoot);
2190
+ let simplifiedMeshCount = 0;
2191
+ simplifiedRoot.traverse((child) => {
2192
+ if (!(child instanceof Mesh3)) {
2193
+ return;
2194
+ }
2195
+ if ("isSkinnedMesh" in child && child.isSkinnedMesh) {
2196
+ return;
2197
+ }
2198
+ const simplifiedGeometry = simplifyModelGeometry(child.geometry, ratio);
2199
+ if (!simplifiedGeometry) {
2200
+ return;
2201
+ }
2202
+ child.geometry = simplifiedGeometry;
2203
+ simplifiedMeshCount += 1;
2204
+ });
2205
+ return simplifiedMeshCount > 0 ? simplifiedRoot : void 0;
2206
+ }
2207
+ function expandGroupedModelMeshesForLodBake(root) {
2208
+ const replacements = [];
2209
+ root.traverse((child) => {
2210
+ if (!(child instanceof Mesh3) || !Array.isArray(child.material) || child.geometry.groups.length <= 1 || !child.parent) {
2211
+ return;
2212
+ }
2213
+ const container = new Group3();
2214
+ container.name = child.name ? `${child.name}:lod-groups` : "lod-groups";
2215
+ container.position.copy(child.position);
2216
+ container.quaternion.copy(child.quaternion);
2217
+ container.scale.copy(child.scale);
2218
+ container.visible = child.visible;
2219
+ container.renderOrder = child.renderOrder;
2220
+ container.userData = structuredClone(child.userData ?? {});
2221
+ child.geometry.groups.forEach((group, groupIndex) => {
2222
+ const material = child.material[group.materialIndex] ?? child.material[0];
2223
+ if (!material) {
2224
+ return;
2225
+ }
2226
+ const partGeometry = extractGeometryGroup(child.geometry, group.start, group.count);
2227
+ const partMesh = new Mesh3(partGeometry, material);
2228
+ partMesh.name = child.name ? `${child.name}:group:${groupIndex}` : `group:${groupIndex}`;
2229
+ partMesh.castShadow = child.castShadow;
2230
+ partMesh.receiveShadow = child.receiveShadow;
2231
+ partMesh.userData = structuredClone(child.userData ?? {});
2232
+ container.add(partMesh);
2233
+ });
2234
+ replacements.push({
2235
+ container,
2236
+ mesh: child,
2237
+ parent: child.parent
2238
+ });
2239
+ });
2240
+ replacements.forEach(({ container, mesh, parent }) => {
2241
+ parent.add(container);
2242
+ parent.remove(mesh);
2243
+ });
2244
+ }
2245
+ function extractGeometryGroup(geometry, start, count) {
2246
+ const groupGeometry = new BufferGeometry3();
2247
+ const index = geometry.getIndex();
2248
+ const attributes = geometry.attributes;
2249
+ Object.entries(attributes).forEach(([name, attribute]) => {
2250
+ groupGeometry.setAttribute(name, attribute);
2251
+ });
2252
+ if (index) {
2253
+ groupGeometry.setIndex(Array.from(index.array).slice(start, start + count));
2254
+ } else {
2255
+ groupGeometry.setIndex(Array.from({ length: count }, (_, offset) => start + offset));
2256
+ }
2257
+ groupGeometry.computeBoundingBox();
2258
+ groupGeometry.computeBoundingSphere();
2259
+ return groupGeometry;
2260
+ }
2261
+ function simplifyModelGeometry(geometry, ratio) {
2262
+ const positionAttribute = geometry.getAttribute("position");
2263
+ const vertexCount = positionAttribute?.count ?? 0;
2264
+ if (!positionAttribute || vertexCount < 12 || ratio >= 0.98) {
2265
+ return void 0;
2266
+ }
2267
+ const workingGeometry = geometry.getAttribute("normal") ? geometry : geometry.clone();
2268
+ if (!workingGeometry.getAttribute("normal")) {
2269
+ workingGeometry.computeVertexNormals();
2270
+ }
2271
+ workingGeometry.computeBoundingBox();
2272
+ const bounds = workingGeometry.boundingBox?.clone();
2273
+ if (!bounds) {
2274
+ if (workingGeometry !== geometry) {
2275
+ workingGeometry.dispose();
2276
+ }
2277
+ return void 0;
2278
+ }
2279
+ const normalAttribute = workingGeometry.getAttribute("normal");
2280
+ const uvAttribute = workingGeometry.getAttribute("uv");
2281
+ const index = workingGeometry.getIndex();
2282
+ const simplified = simplifyPrimitiveWithVertexClustering(
2283
+ {
2284
+ indices: index ? Array.from(index.array) : Array.from({ length: vertexCount }, (_, value) => value),
2285
+ material: {
2286
+ color: "#ffffff",
2287
+ id: "material:model-simplify",
2288
+ metallicFactor: 0,
2289
+ name: "Model Simplify",
2290
+ roughnessFactor: 1
2291
+ },
2292
+ normals: Array.from(normalAttribute.array),
2293
+ positions: Array.from(positionAttribute.array),
2294
+ uvs: uvAttribute ? Array.from(uvAttribute.array) : []
2295
+ },
2296
+ ratio,
2297
+ bounds
2298
+ );
2299
+ if (workingGeometry !== geometry) {
2300
+ workingGeometry.dispose();
2301
+ }
2302
+ if (!simplified) {
2303
+ return void 0;
2304
+ }
2305
+ const simplifiedGeometry = createBufferGeometryFromPrimitive(simplified);
2306
+ simplifiedGeometry.computeBoundingBox();
2307
+ simplifiedGeometry.computeBoundingSphere();
2308
+ return simplifiedGeometry;
2309
+ }
2310
+ async function exportModelSceneAsGlb(object) {
2311
+ try {
2312
+ return await exportGlbBytesFromObject(object);
2313
+ } catch {
2314
+ return await exportGlbBytesFromObject(stripTextureReferencesFromObject(object.clone(true)));
2315
+ }
2316
+ }
2317
+ async function exportGlbBytesFromObject(object) {
2318
+ const scene = new Scene2();
2319
+ scene.add(object);
2320
+ const exported = await gltfExporter.parseAsync(scene, {
2321
+ binary: true,
2322
+ includeCustomExtensions: false
2323
+ });
2324
+ if (!(exported instanceof ArrayBuffer)) {
2325
+ throw new Error("Expected GLB binary output for baked model LOD.");
2326
+ }
2327
+ return new Uint8Array(exported);
2328
+ }
2329
+ function stripTextureReferencesFromObject(object) {
2330
+ object.traverse((child) => {
2331
+ if (!(child instanceof Mesh3)) {
2332
+ return;
2333
+ }
2334
+ const strip = (material) => {
2335
+ const clone = material.clone();
2336
+ clone.alphaMap = null;
2337
+ clone.aoMap = null;
2338
+ clone.bumpMap = null;
2339
+ clone.displacementMap = null;
2340
+ clone.emissiveMap = null;
2341
+ clone.lightMap = null;
2342
+ clone.map = null;
2343
+ clone.metalnessMap = null;
2344
+ clone.normalMap = null;
2345
+ clone.roughnessMap = null;
2346
+ return clone;
2347
+ };
2348
+ if (Array.isArray(child.material)) {
2349
+ child.material = child.material.map(
2350
+ (material) => material instanceof MeshStandardMaterial3 ? strip(material) : new MeshStandardMaterial3({
2351
+ color: "color" in material ? material.color : "#7f8ea3",
2352
+ metalness: "metalness" in material && typeof material.metalness === "number" ? material.metalness : 0.1,
2353
+ roughness: "roughness" in material && typeof material.roughness === "number" ? material.roughness : 0.8
2354
+ })
2355
+ );
2356
+ return;
2357
+ }
2358
+ const fallbackMaterial = child.material;
2359
+ child.material = child.material instanceof MeshStandardMaterial3 ? strip(child.material) : new MeshStandardMaterial3({
2360
+ color: "color" in fallbackMaterial ? fallbackMaterial.color : "#7f8ea3",
2361
+ metalness: "metalness" in fallbackMaterial && typeof fallbackMaterial.metalness === "number" ? fallbackMaterial.metalness : 0.1,
2362
+ roughness: "roughness" in fallbackMaterial && typeof fallbackMaterial.roughness === "number" ? fallbackMaterial.roughness : 0.8
2363
+ });
2364
+ });
2365
+ return object;
2366
+ }
2367
+ function createGeneratedModelLodAsset(asset, name, nodeId, level, bytes) {
2368
+ return {
2369
+ id: `asset:model-lod:${slugify2(`${name}-${nodeId}`)}:${level}`,
2370
+ metadata: {
2371
+ ...asset.metadata,
2372
+ lodGenerated: true,
2373
+ lodLevel: level,
2374
+ lodSourceAssetId: asset.id,
2375
+ materialMtlText: "",
2376
+ modelFormat: "glb",
2377
+ texturePath: ""
2378
+ },
2379
+ path: createBinaryDataUrl(bytes, "model/gltf-binary"),
2380
+ type: "model"
2381
+ };
2382
+ }
2383
+ function createBinaryDataUrl(bytes, mimeType) {
2384
+ let binary = "";
2385
+ const chunkSize = 32768;
2386
+ for (let index = 0; index < bytes.length; index += chunkSize) {
2387
+ binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
2388
+ }
2389
+ return `data:${mimeType};base64,${btoa(binary)}`;
2390
+ }
2391
+ function sanitizeInstanceTransform(transform) {
2392
+ return {
2393
+ position: structuredClone(transform.position),
2394
+ rotation: structuredClone(transform.rotation),
2395
+ scale: structuredClone(transform.scale)
2396
+ };
2397
+ }
2398
+ function resolveModelAssetFormat(asset) {
2399
+ const format = readModelAssetString(asset, "modelFormat")?.toLowerCase();
2400
+ return format === "obj" || asset.path.toLowerCase().endsWith(".obj") ? "obj" : "gltf";
2401
+ }
2402
+ function readModelAssetString(asset, key) {
2403
+ const value = asset?.metadata[key];
2404
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2405
+ }
2406
+ function patchMtlTextureReferences2(mtlText, texturePath) {
2407
+ if (!texturePath) {
2408
+ return mtlText;
2409
+ }
2410
+ const mapPattern = /^(map_Ka|map_Kd|map_d|map_Bump|bump)\s+.+$/gm;
2411
+ const hasDiffuseMap = /^map_Kd\s+.+$/m.test(mtlText);
2412
+ const normalized = mtlText.replace(mapPattern, (line) => {
2413
+ if (line.startsWith("map_Kd ")) {
2414
+ return `map_Kd ${texturePath}`;
2415
+ }
2416
+ return line;
2417
+ });
2418
+ return hasDiffuseMap ? normalized : `${normalized.trim()}
2419
+ map_Kd ${texturePath}
2420
+ `;
2421
+ }
2422
+ function slugify2(value) {
2423
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
2424
+ return normalized || "model";
2425
+ }
2426
+ function simplifyExportGeometry(geometry, ratio) {
2427
+ const primitives = geometry.primitives.map((primitive) => simplifyExportPrimitive(primitive, ratio)).filter((primitive) => primitive !== void 0);
2428
+ return primitives.length ? { primitives } : void 0;
2429
+ }
2430
+ function simplifyExportPrimitive(primitive, ratio) {
2431
+ const vertexCount = Math.floor(primitive.positions.length / 3);
2432
+ const triangleCount = Math.floor(primitive.indices.length / 3);
2433
+ if (vertexCount < 12 || triangleCount < 8 || ratio >= 0.98) {
2434
+ return void 0;
2435
+ }
2436
+ const geometry = createBufferGeometryFromPrimitive(primitive);
2437
+ const boundsTree = new MeshBVH(geometry, { maxLeafSize: 12, setBoundingBox: true });
2438
+ const bounds = boundsTree.getBoundingBox(new Box33());
2439
+ const simplified = simplifyPrimitiveWithVertexClustering(primitive, ratio, bounds);
2440
+ geometry.dispose();
2441
+ if (!simplified) {
2442
+ return void 0;
2443
+ }
2444
+ return simplified;
2445
+ }
2446
+ function createBufferGeometryFromPrimitive(primitive) {
2447
+ const geometry = new BufferGeometry3();
2448
+ geometry.setAttribute("position", new Float32BufferAttribute3(primitive.positions, 3));
2449
+ geometry.setAttribute("normal", new Float32BufferAttribute3(primitive.normals, 3));
2450
+ if (primitive.uvs.length) {
2451
+ geometry.setAttribute("uv", new Float32BufferAttribute3(primitive.uvs, 2));
2452
+ }
2453
+ geometry.setIndex(primitive.indices);
2454
+ return geometry;
2455
+ }
2456
+ function simplifyPrimitiveWithVertexClustering(primitive, ratio, bounds) {
2457
+ const targetVertexCount = Math.max(8, Math.floor(primitive.positions.length / 3 * Math.max(0.04, ratio)));
2458
+ const size = bounds.getSize(new Vector34());
2459
+ let resolution = Math.max(1, Math.round(Math.cbrt(targetVertexCount)));
2460
+ let best;
2461
+ for (let attempt = 0; attempt < 5; attempt += 1) {
2462
+ const simplified = clusterPrimitiveVertices(primitive, bounds, size, Math.max(1, resolution - attempt));
2463
+ if (!simplified) {
2464
+ continue;
2465
+ }
2466
+ best = simplified;
2467
+ if (simplified.positions.length / 3 <= targetVertexCount) {
2468
+ break;
2469
+ }
2470
+ }
2471
+ if (!best) {
2472
+ return void 0;
2473
+ }
2474
+ if (best.positions.length >= primitive.positions.length || best.indices.length >= primitive.indices.length) {
2475
+ return void 0;
2476
+ }
2477
+ return best;
2478
+ }
2479
+ function clusterPrimitiveVertices(primitive, bounds, size, resolution) {
2480
+ const min = bounds.min;
2481
+ const cellSizeX = Math.max(size.x / resolution, 1e-4);
2482
+ const cellSizeY = Math.max(size.y / resolution, 1e-4);
2483
+ const cellSizeZ = Math.max(size.z / resolution, 1e-4);
2484
+ const vertexCount = primitive.positions.length / 3;
2485
+ const clusters = /* @__PURE__ */ new Map();
2486
+ const clusterKeyByVertex = new Array(vertexCount);
2487
+ for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex += 1) {
2488
+ const positionOffset = vertexIndex * 3;
2489
+ const uvOffset = vertexIndex * 2;
2490
+ const x = primitive.positions[positionOffset];
2491
+ const y = primitive.positions[positionOffset + 1];
2492
+ const z = primitive.positions[positionOffset + 2];
2493
+ const normalX = primitive.normals[positionOffset];
2494
+ const normalY = primitive.normals[positionOffset + 1];
2495
+ const normalZ = primitive.normals[positionOffset + 2];
2496
+ const cellX = Math.floor((x - min.x) / cellSizeX);
2497
+ const cellY = Math.floor((y - min.y) / cellSizeY);
2498
+ const cellZ = Math.floor((z - min.z) / cellSizeZ);
2499
+ const clusterKey = `${cellX}:${cellY}:${cellZ}:${resolveNormalBucket(normalX, normalY, normalZ)}`;
2500
+ const cluster = clusters.get(clusterKey) ?? {
2501
+ count: 0,
2502
+ normalX: 0,
2503
+ normalY: 0,
2504
+ normalZ: 0,
2505
+ positionX: 0,
2506
+ positionY: 0,
2507
+ positionZ: 0,
2508
+ uvX: 0,
2509
+ uvY: 0
2510
+ };
2511
+ cluster.count += 1;
2512
+ cluster.positionX += x;
2513
+ cluster.positionY += y;
2514
+ cluster.positionZ += z;
2515
+ cluster.normalX += normalX;
2516
+ cluster.normalY += normalY;
2517
+ cluster.normalZ += normalZ;
2518
+ cluster.uvX += primitive.uvs[uvOffset] ?? 0;
2519
+ cluster.uvY += primitive.uvs[uvOffset + 1] ?? 0;
2520
+ clusters.set(clusterKey, cluster);
2521
+ clusterKeyByVertex[vertexIndex] = clusterKey;
2522
+ }
2523
+ const remappedIndices = [];
2524
+ const positions = [];
2525
+ const normals = [];
2526
+ const uvs = [];
2527
+ const clusterIndexByKey = /* @__PURE__ */ new Map();
2528
+ const ensureClusterIndex = (clusterKey) => {
2529
+ const existing = clusterIndexByKey.get(clusterKey);
2530
+ if (existing !== void 0) {
2531
+ return existing;
2532
+ }
2533
+ const cluster = clusters.get(clusterKey);
2534
+ if (!cluster || cluster.count === 0) {
2535
+ return void 0;
2536
+ }
2537
+ const averagedNormal = normalizeVec3(vec3(cluster.normalX, cluster.normalY, cluster.normalZ));
2538
+ const index = positions.length / 3;
2539
+ positions.push(cluster.positionX / cluster.count, cluster.positionY / cluster.count, cluster.positionZ / cluster.count);
2540
+ normals.push(averagedNormal.x, averagedNormal.y, averagedNormal.z);
2541
+ uvs.push(cluster.uvX / cluster.count, cluster.uvY / cluster.count);
2542
+ clusterIndexByKey.set(clusterKey, index);
2543
+ return index;
2544
+ };
2545
+ for (let index = 0; index < primitive.indices.length; index += 3) {
2546
+ const a = ensureClusterIndex(clusterKeyByVertex[primitive.indices[index]]);
2547
+ const b = ensureClusterIndex(clusterKeyByVertex[primitive.indices[index + 1]]);
2548
+ const c = ensureClusterIndex(clusterKeyByVertex[primitive.indices[index + 2]]);
2549
+ if (a === void 0 || b === void 0 || c === void 0) {
2550
+ continue;
2551
+ }
2552
+ if (a === b || b === c || a === c) {
2553
+ continue;
2554
+ }
2555
+ if (triangleArea(positions, a, b, c) <= 1e-6) {
2556
+ continue;
2557
+ }
2558
+ remappedIndices.push(a, b, c);
2559
+ }
2560
+ if (remappedIndices.length < 12 || positions.length >= primitive.positions.length) {
2561
+ return void 0;
2562
+ }
2563
+ return {
2564
+ indices: remappedIndices,
2565
+ material: primitive.material,
2566
+ normals,
2567
+ positions,
2568
+ uvs
2569
+ };
2570
+ }
2571
+ function resolveNormalBucket(x, y, z) {
2572
+ const ax = Math.abs(x);
2573
+ const ay = Math.abs(y);
2574
+ const az = Math.abs(z);
2575
+ if (ax >= ay && ax >= az) {
2576
+ return x >= 0 ? "xp" : "xn";
2577
+ }
2578
+ if (ay >= ax && ay >= az) {
2579
+ return y >= 0 ? "yp" : "yn";
2580
+ }
2581
+ return z >= 0 ? "zp" : "zn";
2582
+ }
2583
+ function triangleArea(positions, a, b, c) {
2584
+ const ax = positions[a * 3];
2585
+ const ay = positions[a * 3 + 1];
2586
+ const az = positions[a * 3 + 2];
2587
+ const bx = positions[b * 3];
2588
+ const by = positions[b * 3 + 1];
2589
+ const bz = positions[b * 3 + 2];
2590
+ const cx = positions[c * 3];
2591
+ const cy = positions[c * 3 + 1];
2592
+ const cz = positions[c * 3 + 2];
2593
+ const ab = vec3(bx - ax, by - ay, bz - az);
2594
+ const ac = vec3(cx - ax, cy - ay, cz - az);
2595
+ const cross = crossVec3(ab, ac);
2596
+ return Math.sqrt(cross.x * cross.x + cross.y * cross.y + cross.z * cross.z) * 0.5;
2597
+ }
2598
+ function buildPrimitiveGeometry(shape, size, radialSegments) {
2599
+ const geometry = shape === "cube" ? new BoxGeometry3(Math.abs(size.x), Math.abs(size.y), Math.abs(size.z)) : shape === "sphere" ? new SphereGeometry(Math.max(Math.abs(size.x), Math.abs(size.z)) * 0.5, radialSegments, Math.max(8, Math.floor(radialSegments * 0.75))) : shape === "cylinder" ? new CylinderGeometry(Math.max(Math.abs(size.x), Math.abs(size.z)) * 0.5, Math.max(Math.abs(size.x), Math.abs(size.z)) * 0.5, Math.abs(size.y), radialSegments) : new ConeGeometry(Math.max(Math.abs(size.x), Math.abs(size.z)) * 0.5, Math.abs(size.y), radialSegments);
2600
+ const positionAttribute = geometry.getAttribute("position");
2601
+ const normalAttribute = geometry.getAttribute("normal");
2602
+ const uvAttribute = geometry.getAttribute("uv");
2603
+ const index = geometry.getIndex();
2604
+ const primitive = {
2605
+ indices: index ? Array.from(index.array) : Array.from({ length: positionAttribute.count }, (_, value) => value),
2606
+ normals: Array.from(normalAttribute.array),
2607
+ positions: Array.from(positionAttribute.array),
2608
+ uvs: uvAttribute ? Array.from(uvAttribute.array) : []
2609
+ };
2610
+ geometry.dispose();
2611
+ return primitive;
2612
+ }
2613
+ async function resolveRuntimeMaterial(material) {
2614
+ const resolved = material ?? {
2615
+ color: "#ffffff",
2616
+ id: "material:fallback:default",
2617
+ metalness: 0.05,
2618
+ name: "Default Material",
2619
+ roughness: 0.8
2620
+ };
2621
+ return {
2622
+ baseColorTexture: await resolveEmbeddedTextureUri(resolved.colorTexture ?? resolveGeneratedBlockoutTexture(resolved)),
2623
+ color: resolved.color,
2624
+ id: resolved.id,
2625
+ metallicFactor: resolved.metalness ?? 0,
2626
+ metallicRoughnessTexture: await createMetallicRoughnessTextureDataUri(
2627
+ resolved.metalnessTexture,
2628
+ resolved.roughnessTexture,
2629
+ resolved.metalness ?? 0,
2630
+ resolved.roughness ?? 0.8
2631
+ ),
2632
+ name: resolved.name,
2633
+ normalTexture: await resolveEmbeddedTextureUri(resolved.normalTexture),
2634
+ roughnessFactor: resolved.roughness ?? 0.8,
2635
+ side: resolved.side
2636
+ };
2637
+ }
2638
+ function resolveGeneratedBlockoutTexture(material) {
2639
+ return material.category === "blockout" ? createBlockoutTextureDataUri(material.color, material.edgeColor ?? "#2f3540", material.edgeThickness ?? 0.035) : void 0;
2640
+ }
2641
+ function projectPlanarUvs(vertices, normal, uvScale, uvOffset) {
2642
+ const basis = createFacePlaneBasis(normal);
2643
+ const origin = vertices[0] ?? vec3(0, 0, 0);
2644
+ const scaleX = Math.abs(uvScale?.x ?? 1) <= 1e-4 ? 1 : uvScale?.x ?? 1;
2645
+ const scaleY = Math.abs(uvScale?.y ?? 1) <= 1e-4 ? 1 : uvScale?.y ?? 1;
2646
+ const offsetX = uvOffset?.x ?? 0;
2647
+ const offsetY = uvOffset?.y ?? 0;
2648
+ return vertices.flatMap((vertex) => {
2649
+ const offset = subVec3(vertex, origin);
2650
+ return [dotVec3(offset, basis.u) * scaleX + offsetX, dotVec3(offset, basis.v) * scaleY + offsetY];
2651
+ });
2652
+ }
2653
+ function createFacePlaneBasis(normal) {
2654
+ const normalizedNormal = normalizeVec3(normal);
2655
+ const reference = Math.abs(normalizedNormal.y) < 0.99 ? vec3(0, 1, 0) : vec3(1, 0, 0);
2656
+ const u = normalizeVec3(crossVec3(reference, normalizedNormal));
2657
+ const v = normalizeVec3(crossVec3(normalizedNormal, u));
2658
+ return { u, v };
2659
+ }
2660
+ async function resolveEmbeddedTextureUri(source) {
2661
+ if (!source) {
2662
+ return void 0;
2663
+ }
2664
+ if (source.startsWith("data:")) {
2665
+ return source;
2666
+ }
2667
+ const response = await fetch(source);
2668
+ const blob = await response.blob();
2669
+ const buffer = await blob.arrayBuffer();
2670
+ return `data:${blob.type || "application/octet-stream"};base64,${toBase64(new Uint8Array(buffer))}`;
2671
+ }
2672
+ async function createMetallicRoughnessTextureDataUri(metalnessSource, roughnessSource, metalnessFactor, roughnessFactor) {
2673
+ if (!metalnessSource && !roughnessSource) {
2674
+ return void 0;
2675
+ }
2676
+ if (typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {
2677
+ return void 0;
2678
+ }
2679
+ const [metalness, roughness] = await Promise.all([
2680
+ loadImagePixels(metalnessSource),
2681
+ loadImagePixels(roughnessSource)
2682
+ ]);
2683
+ const width = Math.max(metalness?.width ?? 1, roughness?.width ?? 1);
2684
+ const height = Math.max(metalness?.height ?? 1, roughness?.height ?? 1);
2685
+ const canvas = new OffscreenCanvas(width, height);
2686
+ const context = canvas.getContext("2d");
2687
+ if (!context) {
2688
+ return void 0;
2689
+ }
2690
+ const imageData = context.createImageData(width, height);
2691
+ const metalDefault = Math.round(clamp01(metalnessFactor) * 255);
2692
+ const roughDefault = Math.round(clamp01(roughnessFactor) * 255);
2693
+ for (let index = 0; index < imageData.data.length; index += 4) {
2694
+ imageData.data[index] = 0;
2695
+ imageData.data[index + 1] = roughness?.pixels[index] ?? roughDefault;
2696
+ imageData.data[index + 2] = metalness?.pixels[index] ?? metalDefault;
2697
+ imageData.data[index + 3] = 255;
2698
+ }
2699
+ context.putImageData(imageData, 0, 0);
2700
+ const blob = await canvas.convertToBlob({ type: "image/png" });
2701
+ const buffer = await blob.arrayBuffer();
2702
+ return `data:image/png;base64,${toBase64(new Uint8Array(buffer))}`;
2703
+ }
2704
+ async function loadImagePixels(source) {
2705
+ if (!source || typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {
2706
+ return void 0;
2707
+ }
2708
+ const response = await fetch(source);
2709
+ const blob = await response.blob();
2710
+ const bitmap = await createImageBitmap(blob);
2711
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
2712
+ const context = canvas.getContext("2d", { willReadFrequently: true });
2713
+ if (!context) {
2714
+ bitmap.close();
2715
+ return void 0;
2716
+ }
2717
+ context.drawImage(bitmap, 0, 0);
2718
+ bitmap.close();
2719
+ const imageData = context.getImageData(0, 0, bitmap.width, bitmap.height);
2720
+ return {
2721
+ height: imageData.height,
2722
+ pixels: imageData.data,
2723
+ width: imageData.width
2724
+ };
2725
+ }
2726
+ function clamp01(value) {
2727
+ return Math.max(0, Math.min(1, value));
2728
+ }
2729
+ function toBase64(bytes) {
2730
+ let binary = "";
2731
+ bytes.forEach((byte) => {
2732
+ binary += String.fromCharCode(byte);
2733
+ });
2734
+ return btoa(binary);
2735
+ }
2736
+
2737
+ // src/bundle.ts
2738
+ function createThreeAssetResolver(bundle) {
2739
+ const urlByPath = /* @__PURE__ */ new Map();
2740
+ return {
2741
+ dispose() {
2742
+ urlByPath.forEach((url) => {
2743
+ URL.revokeObjectURL(url);
2744
+ });
2745
+ urlByPath.clear();
2746
+ },
2747
+ resolve(path) {
2748
+ const existing = urlByPath.get(path);
2749
+ if (existing) {
2750
+ return existing;
2751
+ }
2752
+ const file = bundle.files.find((entry) => entry.path === path);
2753
+ if (!file) {
2754
+ return path;
2755
+ }
2756
+ const bytes = new Uint8Array(file.bytes.byteLength);
2757
+ bytes.set(file.bytes);
2758
+ const url = URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimeType }));
2759
+ urlByPath.set(path, url);
2760
+ return url;
2761
+ }
2762
+ };
2763
+ }
2764
+ function createWebHammerBundleAssetResolver(bundle) {
2765
+ return createThreeAssetResolver(bundle);
2766
+ }
2767
+ function createWebHammerEngineBundleZip2(bundle, options) {
2768
+ return createWebHammerEngineBundleZip(bundle, options);
2769
+ }
2770
+ function parseWebHammerEngineBundleZip2(bundleBytes, options) {
2771
+ return parseWebHammerEngineBundleZip(bundleBytes, options);
2772
+ }
2773
+ function externalizeWebHammerEngineScene2(scene, options) {
2774
+ return externalizeWebHammerEngineScene(scene, options);
2775
+ }
2776
+ export {
2777
+ applyRuntimeWorldSettingsToThreeScene,
2778
+ applyWebHammerWorldSettings,
2779
+ buildRuntimeBundle,
2780
+ buildRuntimeScene,
2781
+ buildRuntimeWorldIndex,
2782
+ clearRuntimeWorldSettingsFromThreeScene,
2783
+ clearWebHammerWorldSettings,
2784
+ createThreeAssetResolver,
2785
+ createThreeRuntimeObjectFactory,
2786
+ createThreeRuntimeSceneInstance,
2787
+ createWebHammerBundleAssetResolver,
2788
+ createWebHammerEngineBundleZip2 as createWebHammerEngineBundleZip,
2789
+ createWebHammerSceneObjectFactory,
2790
+ externalizeRuntimeAssets,
2791
+ externalizeWebHammerEngineScene2 as externalizeWebHammerEngineScene,
2792
+ extractPhysics,
2793
+ fetchWebHammerEngineScene,
2794
+ findPrimaryLight,
2795
+ isWebHammerEngineBundle,
2796
+ isWebHammerEngineScene,
2797
+ loadThreeRuntimeSceneInstanceFromUrl,
2798
+ loadWebHammerEngineScene,
2799
+ loadWebHammerEngineSceneFromUrl,
2800
+ packRuntimeBundle,
2801
+ parseWebHammerEngineBundleZip2 as parseWebHammerEngineBundleZip,
2802
+ parseWebHammerEngineScene,
2803
+ unpackRuntimeBundle
2804
+ };
2805
+ //# sourceMappingURL=index.js.map