@ggez/three-runtime 0.1.0 → 0.1.2

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 CHANGED
@@ -1,225 +1,5 @@
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
1
  // src/loader.ts
2
+ import { resolveInstancingSourceNode as resolveInstancingSourceNode2, resolveSceneGraph as resolveSceneGraph2, vec3 as vec32 } from "@ggez/shared";
223
3
  import {
224
4
  AmbientLight as AmbientLight2,
225
5
  BackSide as BackSide2,
@@ -230,7 +10,7 @@ import {
230
10
  DirectionalLight as DirectionalLight2,
231
11
  DoubleSide as DoubleSide2,
232
12
  EquirectangularReflectionMapping,
233
- Euler as Euler3,
13
+ Euler as Euler2,
234
14
  Fog,
235
15
  Float32BufferAttribute as Float32BufferAttribute2,
236
16
  FrontSide as FrontSide2,
@@ -238,108 +18,30 @@ import {
238
18
  InstancedMesh as InstancedMesh2,
239
19
  HemisphereLight as HemisphereLight2,
240
20
  LOD as LOD2,
241
- Matrix4 as Matrix43,
21
+ Matrix4 as Matrix42,
242
22
  Mesh as Mesh2,
243
23
  MeshStandardMaterial as MeshStandardMaterial2,
244
24
  Object3D as Object3D2,
245
25
  PointLight as PointLight2,
246
- Quaternion as Quaternion3,
26
+ Quaternion as Quaternion2,
247
27
  SRGBColorSpace as SRGBColorSpace2,
248
28
  SpotLight as SpotLight2,
249
29
  TextureLoader as TextureLoader2,
250
30
  RepeatWrapping as RepeatWrapping2,
251
- Vector3 as Vector33
31
+ Vector3 as Vector32
252
32
  } from "three";
253
33
  import { GLTFLoader as GLTFLoader2 } from "three/examples/jsm/loaders/GLTFLoader.js";
254
34
  import { HDRLoader } from "three/examples/jsm/loaders/HDRLoader.js";
255
35
  import { MTLLoader as MTLLoader2 } from "three/examples/jsm/loaders/MTLLoader.js";
256
36
  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
- }
37
+ import {
38
+ isRuntimeBundle,
39
+ isRuntimeScene,
40
+ parseRuntimeScene
41
+ } from "@ggez/runtime-format";
341
42
 
342
43
  // src/object-factory.ts
44
+ import { resolveInstancingSourceNode, resolveSceneGraph, vec3 } from "@ggez/shared";
343
45
  import {
344
46
  AmbientLight,
345
47
  BackSide,
@@ -348,24 +50,24 @@ import {
348
50
  BufferGeometry,
349
51
  DirectionalLight,
350
52
  DoubleSide,
351
- Euler as Euler2,
53
+ Euler,
352
54
  Float32BufferAttribute,
353
55
  FrontSide,
354
56
  Group,
355
57
  HemisphereLight,
356
58
  InstancedMesh,
357
59
  LOD,
358
- Matrix4 as Matrix42,
60
+ Matrix4,
359
61
  Mesh,
360
62
  MeshStandardMaterial,
361
63
  Object3D,
362
64
  PointLight,
363
- Quaternion as Quaternion2,
65
+ Quaternion,
364
66
  RepeatWrapping,
365
67
  SRGBColorSpace,
366
68
  SpotLight,
367
69
  TextureLoader,
368
- Vector3 as Vector32
70
+ Vector3
369
71
  } from "three";
370
72
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
371
73
  import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader.js";
@@ -374,12 +76,12 @@ var textureLoader = new TextureLoader();
374
76
  var gltfLoader = new GLTFLoader();
375
77
  var mtlLoader = new MTLLoader();
376
78
  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();
79
+ var tempModelInstanceMatrix = new Matrix4();
80
+ var tempModelChildMatrix = new Matrix4();
81
+ var tempPivotMatrix = new Matrix4();
82
+ var tempInstancePosition = new Vector3();
83
+ var tempInstanceQuaternion = new Quaternion();
84
+ var tempInstanceScale = new Vector3();
383
85
  function createWebHammerSceneObjectFactory(engineScene, options = {}) {
384
86
  const resources = {
385
87
  assetsById: new Map(engineScene.assets.map((asset) => [asset.id, asset])),
@@ -858,7 +560,7 @@ function centerObject(object, center) {
858
560
  }
859
561
  function computeObjectCenter(object) {
860
562
  const box = new Box3().setFromObject(object);
861
- const center = box.getCenter(new Vector32());
563
+ const center = box.getCenter(new Vector3());
862
564
  return {
863
565
  x: center.x,
864
566
  y: center.y,
@@ -939,9 +641,9 @@ function composeGeometryInstanceMatrix(transform, pivot) {
939
641
  }
940
642
  function composeTransformMatrix(transform) {
941
643
  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"));
644
+ tempInstanceQuaternion.setFromEuler(new Euler(transform.rotation.x, transform.rotation.y, transform.rotation.z, "XYZ"));
943
645
  tempInstanceScale.set(transform.scale.x, transform.scale.y, transform.scale.z);
944
- return new Matrix42().compose(tempInstancePosition, tempInstanceQuaternion, tempInstanceScale);
646
+ return new Matrix4().compose(tempInstancePosition, tempInstanceQuaternion, tempInstanceScale);
945
647
  }
946
648
  function resolveSceneLodOptions(lod) {
947
649
  if (!lod) {
@@ -1043,7 +745,7 @@ async function createThreeRuntimeSceneInstance(input, options = {}) {
1043
745
  const lights = [];
1044
746
  const physicsDescriptors = [];
1045
747
  const runtimeNodesById = new Map(engineScene.nodes.map((node) => [node.id, node]));
1046
- const sceneGraph = resolveSceneGraph(engineScene.nodes, engineScene.entities);
748
+ const sceneGraph = resolveSceneGraph2(engineScene.nodes, engineScene.entities);
1047
749
  const objectFactory = createWebHammerSceneObjectFactory(engineScene, options);
1048
750
  const createdObjects = await Promise.all(
1049
751
  engineScene.nodes.map(async (node) => [node.id, await objectFactory.createNodeObject(node)])
@@ -1280,1461 +982,20 @@ async function loadSkyboxTexture(path, skybox) {
1280
982
  return texture;
1281
983
  }
1282
984
 
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
985
  // src/bundle.ts
986
+ import {
987
+ createWebHammerEngineBundleZip as packWebHammerEngineBundle,
988
+ externalizeWebHammerEngineScene as externalizeWebHammerEngineSceneAssets,
989
+ parseWebHammerEngineBundleZip as unpackWebHammerEngineBundle
990
+ } from "@ggez/runtime-build";
991
+ import {
992
+ buildRuntimeBundle,
993
+ buildRuntimeScene,
994
+ buildRuntimeWorldIndex,
995
+ externalizeRuntimeAssets,
996
+ packRuntimeBundle,
997
+ unpackRuntimeBundle
998
+ } from "@ggez/runtime-build";
2738
999
  function createThreeAssetResolver(bundle) {
2739
1000
  const urlByPath = /* @__PURE__ */ new Map();
2740
1001
  return {
@@ -2764,14 +1025,14 @@ function createThreeAssetResolver(bundle) {
2764
1025
  function createWebHammerBundleAssetResolver(bundle) {
2765
1026
  return createThreeAssetResolver(bundle);
2766
1027
  }
2767
- function createWebHammerEngineBundleZip2(bundle, options) {
2768
- return createWebHammerEngineBundleZip(bundle, options);
1028
+ function createWebHammerEngineBundleZip(bundle, options) {
1029
+ return packWebHammerEngineBundle(bundle, options);
2769
1030
  }
2770
- function parseWebHammerEngineBundleZip2(bundleBytes, options) {
2771
- return parseWebHammerEngineBundleZip(bundleBytes, options);
1031
+ function parseWebHammerEngineBundleZip(bundleBytes, options) {
1032
+ return unpackWebHammerEngineBundle(bundleBytes, options);
2772
1033
  }
2773
- function externalizeWebHammerEngineScene2(scene, options) {
2774
- return externalizeWebHammerEngineScene(scene, options);
1034
+ function externalizeWebHammerEngineScene(scene, options) {
1035
+ return externalizeWebHammerEngineSceneAssets(scene, options);
2775
1036
  }
2776
1037
  export {
2777
1038
  applyRuntimeWorldSettingsToThreeScene,
@@ -2785,10 +1046,10 @@ export {
2785
1046
  createThreeRuntimeObjectFactory,
2786
1047
  createThreeRuntimeSceneInstance,
2787
1048
  createWebHammerBundleAssetResolver,
2788
- createWebHammerEngineBundleZip2 as createWebHammerEngineBundleZip,
1049
+ createWebHammerEngineBundleZip,
2789
1050
  createWebHammerSceneObjectFactory,
2790
1051
  externalizeRuntimeAssets,
2791
- externalizeWebHammerEngineScene2 as externalizeWebHammerEngineScene,
1052
+ externalizeWebHammerEngineScene,
2792
1053
  extractPhysics,
2793
1054
  fetchWebHammerEngineScene,
2794
1055
  findPrimaryLight,
@@ -2798,7 +1059,7 @@ export {
2798
1059
  loadWebHammerEngineScene,
2799
1060
  loadWebHammerEngineSceneFromUrl,
2800
1061
  packRuntimeBundle,
2801
- parseWebHammerEngineBundleZip2 as parseWebHammerEngineBundleZip,
1062
+ parseWebHammerEngineBundleZip,
2802
1063
  parseWebHammerEngineScene,
2803
1064
  unpackRuntimeBundle
2804
1065
  };