@ggez/workers 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,2320 @@
1
+ // src/manager.ts
2
+ function createWorkerTaskManager() {
3
+ let counter = 0;
4
+ const jobs = /* @__PURE__ */ new Map();
5
+ const listeners = /* @__PURE__ */ new Set();
6
+ const emit = () => {
7
+ const snapshot = Array.from(jobs.values());
8
+ listeners.forEach((listener) => {
9
+ listener(snapshot);
10
+ });
11
+ };
12
+ return {
13
+ enqueue(task, label, durationMs = 900) {
14
+ const id = `job:${counter++}`;
15
+ jobs.set(id, {
16
+ id,
17
+ label,
18
+ status: "queued",
19
+ task
20
+ });
21
+ emit();
22
+ queueMicrotask(() => {
23
+ const job = jobs.get(id);
24
+ if (!job) {
25
+ return;
26
+ }
27
+ job.status = "running";
28
+ emit();
29
+ window.setTimeout(() => {
30
+ const runningJob = jobs.get(id);
31
+ if (!runningJob) {
32
+ return;
33
+ }
34
+ runningJob.status = "completed";
35
+ emit();
36
+ window.setTimeout(() => {
37
+ jobs.delete(id);
38
+ emit();
39
+ }, 900);
40
+ }, durationMs);
41
+ });
42
+ return id;
43
+ },
44
+ getJobs() {
45
+ return Array.from(jobs.values());
46
+ },
47
+ subscribe(listener) {
48
+ listeners.add(listener);
49
+ listener(Array.from(jobs.values()));
50
+ return () => {
51
+ listeners.delete(listener);
52
+ };
53
+ }
54
+ };
55
+ }
56
+
57
+ // ../shared/src/utils.ts
58
+ import { Euler, Matrix4, Quaternion, Vector3 } from "three";
59
+ function createBlockoutTextureDataUri(color, edgeColor = "#f5f2ea", edgeThickness = 0.018) {
60
+ const size = 256;
61
+ const frame = Math.max(2, Math.min(6, Math.round(size * edgeThickness)));
62
+ const innerInset = frame + 3;
63
+ const seamInset = innerInset + 5;
64
+ const corner = 18;
65
+ const highlight = mixHexColors(edgeColor, "#ffffff", 0.42);
66
+ const frameColor = mixHexColors(edgeColor, color, 0.12);
67
+ const innerShadow = mixHexColors(edgeColor, color, 0.28);
68
+ const svg = `
69
+ <svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
70
+ <rect width="${size}" height="${size}" rx="${corner}" fill="${color}" />
71
+ <rect x="${frame / 2}" y="${frame / 2}" width="${size - frame}" height="${size - frame}" rx="${corner - 2}" fill="none" stroke="${frameColor}" stroke-width="${frame}" />
72
+ <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" />
73
+ <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" />
74
+ <path d="M ${innerInset} ${size * 0.28} H ${size - innerInset}" stroke="${highlight}" stroke-opacity="0.08" stroke-width="1" />
75
+ <path d="M ${size * 0.28} ${innerInset} V ${size - innerInset}" stroke="${highlight}" stroke-opacity="0.06" stroke-width="1" />
76
+ </svg>
77
+ `.trim();
78
+ return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
79
+ }
80
+ function vec3(x, y, z) {
81
+ return { x, y, z };
82
+ }
83
+ function mixHexColors(left, right, t) {
84
+ const normalizedLeft = normalizeHex(left);
85
+ const normalizedRight = normalizeHex(right);
86
+ const leftValue = Number.parseInt(normalizedLeft.slice(1), 16);
87
+ const rightValue = Number.parseInt(normalizedRight.slice(1), 16);
88
+ const channels = [16, 8, 0].map((shift) => {
89
+ const leftChannel = leftValue >> shift & 255;
90
+ const rightChannel = rightValue >> shift & 255;
91
+ return Math.round(leftChannel + (rightChannel - leftChannel) * t).toString(16).padStart(2, "0");
92
+ });
93
+ return `#${channels.join("")}`;
94
+ }
95
+ function normalizeHex(color) {
96
+ if (/^#[0-9a-f]{6}$/i.test(color)) {
97
+ return color;
98
+ }
99
+ if (/^#[0-9a-f]{3}$/i.test(color)) {
100
+ return `#${color.slice(1).split("").map((channel) => `${channel}${channel}`).join("")}`;
101
+ }
102
+ return "#808080";
103
+ }
104
+ function addVec3(left, right) {
105
+ return vec3(left.x + right.x, left.y + right.y, left.z + right.z);
106
+ }
107
+ function subVec3(left, right) {
108
+ return vec3(left.x - right.x, left.y - right.y, left.z - right.z);
109
+ }
110
+ function scaleVec3(vector, scalar) {
111
+ return vec3(vector.x * scalar, vector.y * scalar, vector.z * scalar);
112
+ }
113
+ function dotVec3(left, right) {
114
+ return left.x * right.x + left.y * right.y + left.z * right.z;
115
+ }
116
+ function crossVec3(left, right) {
117
+ return vec3(
118
+ left.y * right.z - left.z * right.y,
119
+ left.z * right.x - left.x * right.z,
120
+ left.x * right.y - left.y * right.x
121
+ );
122
+ }
123
+ function lengthVec3(vector) {
124
+ return Math.sqrt(dotVec3(vector, vector));
125
+ }
126
+ function normalizeVec3(vector, epsilon = 1e-6) {
127
+ const length = lengthVec3(vector);
128
+ if (length <= epsilon) {
129
+ return vec3(0, 0, 0);
130
+ }
131
+ return scaleVec3(vector, 1 / length);
132
+ }
133
+ function averageVec3(vectors) {
134
+ if (vectors.length === 0) {
135
+ return vec3(0, 0, 0);
136
+ }
137
+ const total = vectors.reduce((sum, vector) => addVec3(sum, vector), vec3(0, 0, 0));
138
+ return scaleVec3(total, 1 / vectors.length);
139
+ }
140
+ var tempPosition = new Vector3();
141
+ var tempQuaternion = new Quaternion();
142
+ var tempScale = new Vector3();
143
+ function isBrushNode(node) {
144
+ return node.kind === "brush";
145
+ }
146
+ function isMeshNode(node) {
147
+ return node.kind === "mesh";
148
+ }
149
+ function isGroupNode(node) {
150
+ return node.kind === "group";
151
+ }
152
+ function isModelNode(node) {
153
+ return node.kind === "model";
154
+ }
155
+ function isPrimitiveNode(node) {
156
+ return node.kind === "primitive";
157
+ }
158
+ function isInstancingNode(node) {
159
+ return node.kind === "instancing";
160
+ }
161
+ function isInstancingSourceNode(node) {
162
+ return isBrushNode(node) || isMeshNode(node) || isPrimitiveNode(node) || isModelNode(node);
163
+ }
164
+ function resolveInstancingSourceNode(nodes, nodeOrId, maxDepth = 32) {
165
+ const nodesById = new Map(Array.from(nodes, (node) => [node.id, node]));
166
+ let current = typeof nodeOrId === "string" ? nodesById.get(nodeOrId) : nodeOrId;
167
+ let depth = 0;
168
+ while (current && depth <= maxDepth) {
169
+ if (isInstancingSourceNode(current)) {
170
+ return current;
171
+ }
172
+ if (!isInstancingNode(current)) {
173
+ return void 0;
174
+ }
175
+ current = nodesById.get(current.data.sourceNodeId);
176
+ depth += 1;
177
+ }
178
+ return void 0;
179
+ }
180
+
181
+ // ../geometry-kernel/src/polygon/polygon-utils.ts
182
+ import earcut from "earcut";
183
+ function computePolygonNormal(vertices) {
184
+ if (vertices.length < 3) {
185
+ return vec3(0, 1, 0);
186
+ }
187
+ let normal = vec3(0, 0, 0);
188
+ for (let index = 0; index < vertices.length; index += 1) {
189
+ const current = vertices[index];
190
+ const next = vertices[(index + 1) % vertices.length];
191
+ normal = addVec3(
192
+ normal,
193
+ vec3(
194
+ (current.y - next.y) * (current.z + next.z),
195
+ (current.z - next.z) * (current.x + next.x),
196
+ (current.x - next.x) * (current.y + next.y)
197
+ )
198
+ );
199
+ }
200
+ const normalized = normalizeVec3(normal);
201
+ return lengthVec3(normalized) === 0 ? vec3(0, 1, 0) : normalized;
202
+ }
203
+ function projectPolygonToPlane(vertices, normal = computePolygonNormal(vertices)) {
204
+ const origin = averageVec3(vertices);
205
+ const tangentReference = Math.abs(normal.y) < 0.99 ? vec3(0, 1, 0) : vec3(1, 0, 0);
206
+ let tangent = normalizeVec3(crossVec3(tangentReference, normal));
207
+ if (lengthVec3(tangent) === 0) {
208
+ tangent = normalizeVec3(crossVec3(vec3(0, 0, 1), normal));
209
+ }
210
+ const bitangent = normalizeVec3(crossVec3(normal, tangent));
211
+ return vertices.map((vertex) => {
212
+ const offset = subVec3(vertex, origin);
213
+ return [dotVec3(offset, tangent), dotVec3(offset, bitangent)];
214
+ });
215
+ }
216
+ function polygonSignedArea(points) {
217
+ let area = 0;
218
+ for (let index = 0; index < points.length; index += 1) {
219
+ const [x1, y1] = points[index];
220
+ const [x2, y2] = points[(index + 1) % points.length];
221
+ area += x1 * y2 - x2 * y1;
222
+ }
223
+ return area * 0.5;
224
+ }
225
+ function sortVerticesOnPlane(vertices, normal) {
226
+ const center = averageVec3(vertices);
227
+ const tangentReference = Math.abs(normal.y) < 0.99 ? vec3(0, 1, 0) : vec3(1, 0, 0);
228
+ let tangent = normalizeVec3(crossVec3(tangentReference, normal));
229
+ if (lengthVec3(tangent) === 0) {
230
+ tangent = normalizeVec3(crossVec3(vec3(0, 0, 1), normal));
231
+ }
232
+ const bitangent = normalizeVec3(crossVec3(normal, tangent));
233
+ const sorted = [...vertices].sort((left, right) => {
234
+ const leftOffset = subVec3(left, center);
235
+ const rightOffset = subVec3(right, center);
236
+ const leftAngle = Math.atan2(dotVec3(leftOffset, bitangent), dotVec3(leftOffset, tangent));
237
+ const rightAngle = Math.atan2(dotVec3(rightOffset, bitangent), dotVec3(rightOffset, tangent));
238
+ return leftAngle - rightAngle;
239
+ });
240
+ if (sorted.length < 3) {
241
+ return sorted;
242
+ }
243
+ const windingNormal = normalizeVec3(
244
+ crossVec3(subVec3(sorted[1], sorted[0]), subVec3(sorted[2], sorted[0]))
245
+ );
246
+ return dotVec3(windingNormal, normal) < 0 ? sorted.reverse() : sorted;
247
+ }
248
+ function triangulatePolygon(points) {
249
+ const flattened = points.flatMap(([x, y]) => [x, y]);
250
+ return earcut(flattened);
251
+ }
252
+ function triangulatePolygon3D(vertices, normal = computePolygonNormal(vertices)) {
253
+ if (vertices.length < 3) {
254
+ return [];
255
+ }
256
+ const projected = projectPolygonToPlane(vertices, normal);
257
+ if (Math.abs(polygonSignedArea(projected)) <= 1e-6) {
258
+ return [];
259
+ }
260
+ return triangulatePolygon(projected);
261
+ }
262
+ function computeFaceCenter(vertices) {
263
+ return averageVec3(vertices);
264
+ }
265
+
266
+ // ../geometry-kernel/src/brush/brush-kernel.ts
267
+ function reconstructBrushFaces(brush, epsilon = 1e-4) {
268
+ if (brush.planes.length < 4) {
269
+ return {
270
+ faces: [],
271
+ vertices: [],
272
+ valid: false,
273
+ errors: ["Brush reconstruction requires at least four planes."]
274
+ };
275
+ }
276
+ const vertexRegistry = /* @__PURE__ */ new Map();
277
+ const faces = [];
278
+ for (let planeIndex = 0; planeIndex < brush.planes.length; planeIndex += 1) {
279
+ const plane = brush.planes[planeIndex];
280
+ const faceVertices = collectFaceVertices(brush.planes, planeIndex, epsilon);
281
+ if (faceVertices.length < 3) {
282
+ continue;
283
+ }
284
+ const orderedVertices = sortVerticesOnPlane(faceVertices, normalizeVec3(plane.normal));
285
+ const triangleIndices = triangulatePolygon3D(orderedVertices, plane.normal);
286
+ if (triangleIndices.length < 3) {
287
+ continue;
288
+ }
289
+ const vertices = orderedVertices.map((position) => registerBrushVertex(vertexRegistry, position, epsilon));
290
+ const seedFace = brush.faces[planeIndex];
291
+ faces.push({
292
+ id: seedFace?.id ?? `face:brush:${planeIndex}`,
293
+ plane,
294
+ materialId: seedFace?.materialId,
295
+ uvOffset: seedFace?.uvOffset,
296
+ uvScale: seedFace?.uvScale,
297
+ vertexIds: vertices.map((vertex) => vertex.id),
298
+ vertices,
299
+ center: computeFaceCenter(orderedVertices),
300
+ normal: normalizeVec3(plane.normal),
301
+ triangleIndices
302
+ });
303
+ }
304
+ return {
305
+ faces,
306
+ vertices: Array.from(vertexRegistry.values()),
307
+ valid: faces.length >= 4,
308
+ errors: faces.length >= 4 ? [] : ["Brush reconstruction did not produce a closed convex solid."]
309
+ };
310
+ }
311
+ function classifyPointAgainstPlane(point, plane, epsilon = 1e-4) {
312
+ const signedDistance = signedDistanceToPlane(point, plane);
313
+ return signedDistance > epsilon ? "outside" : "inside";
314
+ }
315
+ function signedDistanceToPlane(point, plane) {
316
+ return dotVec3(plane.normal, point) - plane.distance;
317
+ }
318
+ function intersectPlanes(first, second, third, epsilon = 1e-6) {
319
+ const denominator = dotVec3(first.normal, crossVec3(second.normal, third.normal));
320
+ if (Math.abs(denominator) <= epsilon) {
321
+ return void 0;
322
+ }
323
+ const firstTerm = scaleVec3(crossVec3(second.normal, third.normal), first.distance);
324
+ const secondTerm = scaleVec3(crossVec3(third.normal, first.normal), second.distance);
325
+ const thirdTerm = scaleVec3(crossVec3(first.normal, second.normal), third.distance);
326
+ return scaleVec3(addVec3(addVec3(firstTerm, secondTerm), thirdTerm), 1 / denominator);
327
+ }
328
+ function collectFaceVertices(planes, planeIndex, epsilon) {
329
+ const plane = planes[planeIndex];
330
+ const vertices = /* @__PURE__ */ new Map();
331
+ for (let firstIndex = 0; firstIndex < planes.length; firstIndex += 1) {
332
+ if (firstIndex === planeIndex) {
333
+ continue;
334
+ }
335
+ for (let secondIndex = firstIndex + 1; secondIndex < planes.length; secondIndex += 1) {
336
+ if (secondIndex === planeIndex) {
337
+ continue;
338
+ }
339
+ const intersection = intersectPlanes(plane, planes[firstIndex], planes[secondIndex], epsilon);
340
+ if (!intersection) {
341
+ continue;
342
+ }
343
+ const liesOnPlane = Math.abs(signedDistanceToPlane(intersection, plane)) <= epsilon * 4;
344
+ const insideAllPlanes = planes.every(
345
+ (candidatePlane) => classifyPointAgainstPlane(intersection, candidatePlane, epsilon * 4) === "inside"
346
+ );
347
+ if (!liesOnPlane || !insideAllPlanes) {
348
+ continue;
349
+ }
350
+ vertices.set(makeVertexKey(intersection, epsilon), intersection);
351
+ }
352
+ }
353
+ return Array.from(vertices.values());
354
+ }
355
+ function registerBrushVertex(registry, position, epsilon) {
356
+ const key = makeVertexKey(position, epsilon);
357
+ const existing = registry.get(key);
358
+ if (existing) {
359
+ return existing;
360
+ }
361
+ const vertex = {
362
+ id: `vertex:brush:${registry.size}`,
363
+ position: vec3(position.x, position.y, position.z)
364
+ };
365
+ registry.set(key, vertex);
366
+ return vertex;
367
+ }
368
+ function makeVertexKey(position, epsilon) {
369
+ return [
370
+ Math.round(position.x / epsilon),
371
+ Math.round(position.y / epsilon),
372
+ Math.round(position.z / epsilon)
373
+ ].join(":");
374
+ }
375
+
376
+ // ../geometry-kernel/src/mesh/editable-mesh.ts
377
+ var editableMeshIndexCache = /* @__PURE__ */ new WeakMap();
378
+ function getFaceVertexIds(mesh, faceId) {
379
+ const index = getEditableMeshIndex(mesh);
380
+ const cachedIds = index.faceVertexIds.get(faceId);
381
+ if (cachedIds) {
382
+ return cachedIds;
383
+ }
384
+ const face = index.faceById.get(faceId);
385
+ if (!face) {
386
+ return [];
387
+ }
388
+ const ids = [];
389
+ let currentEdgeId = face.halfEdge;
390
+ let guard = 0;
391
+ while (currentEdgeId && guard < mesh.halfEdges.length + 1) {
392
+ const halfEdge = index.halfEdgeById.get(currentEdgeId);
393
+ if (!halfEdge) {
394
+ return [];
395
+ }
396
+ ids.push(halfEdge.vertex);
397
+ currentEdgeId = halfEdge.next;
398
+ guard += 1;
399
+ if (currentEdgeId === face.halfEdge) {
400
+ break;
401
+ }
402
+ }
403
+ index.faceVertexIds.set(faceId, ids);
404
+ return ids;
405
+ }
406
+ function getFaceVertices(mesh, faceId) {
407
+ const index = getEditableMeshIndex(mesh);
408
+ return getFaceVertexIds(mesh, faceId).map((vertexId) => index.vertexById.get(vertexId)).filter((vertex) => Boolean(vertex));
409
+ }
410
+ function triangulateMeshFace(mesh, faceId) {
411
+ const faceVertices = getFaceVertices(mesh, faceId);
412
+ if (faceVertices.length < 3) {
413
+ return void 0;
414
+ }
415
+ const normal = computePolygonNormal(faceVertices.map((vertex) => vertex.position));
416
+ const indices = triangulatePolygon3D(
417
+ faceVertices.map((vertex) => vertex.position),
418
+ normal
419
+ );
420
+ if (indices.length < 3) {
421
+ return void 0;
422
+ }
423
+ return {
424
+ faceId,
425
+ vertexIds: faceVertices.map((vertex) => vertex.id),
426
+ normal,
427
+ indices
428
+ };
429
+ }
430
+ function getEditableMeshIndex(mesh) {
431
+ const cached = editableMeshIndexCache.get(mesh);
432
+ if (cached && cached.faces === mesh.faces && cached.halfEdges === mesh.halfEdges && cached.vertices === mesh.vertices) {
433
+ return cached;
434
+ }
435
+ const nextIndex = {
436
+ faceById: new Map(mesh.faces.map((face) => [face.id, face])),
437
+ faceVertexIds: /* @__PURE__ */ new Map(),
438
+ faces: mesh.faces,
439
+ halfEdgeById: new Map(mesh.halfEdges.map((halfEdge) => [halfEdge.id, halfEdge])),
440
+ halfEdges: mesh.halfEdges,
441
+ vertexById: new Map(mesh.vertices.map((vertex) => [vertex.id, vertex])),
442
+ vertices: mesh.vertices
443
+ };
444
+ editableMeshIndexCache.set(mesh, nextIndex);
445
+ return nextIndex;
446
+ }
447
+
448
+ // ../runtime-build/src/bundle.ts
449
+ import { unzipSync, zipSync } from "fflate";
450
+
451
+ // ../runtime-format/src/types.ts
452
+ var CURRENT_RUNTIME_SCENE_VERSION = 6;
453
+
454
+ // ../runtime-build/src/bundle.ts
455
+ var TEXTURE_FIELDS = ["baseColorTexture", "metallicRoughnessTexture", "normalTexture"];
456
+ async function externalizeRuntimeAssets(scene, options = {}) {
457
+ const manifest = structuredClone(scene);
458
+ const files = [];
459
+ const assetDir = trimSlashes(options.assetDir ?? "assets");
460
+ const copyExternalAssets = options.copyExternalAssets ?? true;
461
+ const pathBySource = /* @__PURE__ */ new Map();
462
+ const usedPaths = /* @__PURE__ */ new Set();
463
+ for (const material of manifest.materials) {
464
+ for (const field of TEXTURE_FIELDS) {
465
+ const source = material[field];
466
+ if (!source) {
467
+ continue;
468
+ }
469
+ const bundledPath = await materializeSource(source, {
470
+ copyExternalAssets,
471
+ files,
472
+ pathBySource,
473
+ preferredStem: `${assetDir}/textures/${slugify(material.id)}-${textureFieldSuffix(field)}`,
474
+ usedPaths
475
+ });
476
+ if (bundledPath) {
477
+ material[field] = bundledPath;
478
+ }
479
+ }
480
+ }
481
+ for (const asset of manifest.assets) {
482
+ if (asset.type !== "model") {
483
+ continue;
484
+ }
485
+ const bundledPath = await materializeSource(asset.path, {
486
+ copyExternalAssets,
487
+ files,
488
+ pathBySource,
489
+ preferredExtension: inferModelExtension(asset.path, asset.metadata.modelFormat),
490
+ preferredStem: `${assetDir}/models/${slugify(asset.id)}`,
491
+ usedPaths
492
+ });
493
+ if (bundledPath) {
494
+ asset.path = bundledPath;
495
+ }
496
+ const texturePath = asset.metadata.texturePath;
497
+ if (typeof texturePath === "string" && texturePath.length > 0) {
498
+ const bundledTexturePath = await materializeSource(texturePath, {
499
+ copyExternalAssets,
500
+ files,
501
+ pathBySource,
502
+ preferredStem: `${assetDir}/model-textures/${slugify(asset.id)}`,
503
+ usedPaths
504
+ });
505
+ if (bundledTexturePath) {
506
+ asset.metadata.texturePath = bundledTexturePath;
507
+ }
508
+ }
509
+ }
510
+ const skyboxSource = manifest.settings.world.skybox.source;
511
+ if (skyboxSource) {
512
+ const bundledSkyboxPath = await materializeSource(skyboxSource, {
513
+ copyExternalAssets,
514
+ files,
515
+ pathBySource,
516
+ preferredExtension: manifest.settings.world.skybox.format === "hdr" ? "hdr" : inferExtensionFromPath(skyboxSource),
517
+ preferredStem: `${assetDir}/skyboxes/${slugify(manifest.settings.world.skybox.name || "skybox")}`,
518
+ usedPaths
519
+ });
520
+ if (bundledSkyboxPath) {
521
+ manifest.settings.world.skybox.source = bundledSkyboxPath;
522
+ }
523
+ }
524
+ return {
525
+ files,
526
+ manifest
527
+ };
528
+ }
529
+ async function materializeSource(source, context) {
530
+ const existing = context.pathBySource.get(source);
531
+ if (existing) {
532
+ return existing;
533
+ }
534
+ if (isDataUrl(source)) {
535
+ const parsed = parseDataUrl(source);
536
+ const path2 = ensureUniquePath(
537
+ `${context.preferredStem}.${inferExtension(parsed.mimeType, context.preferredExtension)}`,
538
+ context.usedPaths
539
+ );
540
+ context.files.push({
541
+ bytes: parsed.bytes,
542
+ mimeType: parsed.mimeType,
543
+ path: path2
544
+ });
545
+ context.pathBySource.set(source, path2);
546
+ return path2;
547
+ }
548
+ if (!context.copyExternalAssets) {
549
+ return void 0;
550
+ }
551
+ const response = await fetch(source);
552
+ if (!response.ok) {
553
+ throw new Error(`Failed to bundle asset: ${source}`);
554
+ }
555
+ const blob = await response.blob();
556
+ const bytes = new Uint8Array(await blob.arrayBuffer());
557
+ const path = ensureUniquePath(
558
+ `${context.preferredStem}.${inferExtension(blob.type, context.preferredExtension ?? inferExtensionFromPath(source))}`,
559
+ context.usedPaths
560
+ );
561
+ context.files.push({
562
+ bytes,
563
+ mimeType: blob.type || "application/octet-stream",
564
+ path
565
+ });
566
+ context.pathBySource.set(source, path);
567
+ return path;
568
+ }
569
+ function parseDataUrl(source) {
570
+ const match = /^data:([^;,]+)?(?:;charset=[^;,]+)?(;base64)?,(.*)$/i.exec(source);
571
+ if (!match) {
572
+ throw new Error("Invalid data URL.");
573
+ }
574
+ const mimeType = match[1] || "application/octet-stream";
575
+ const payload = match[3] || "";
576
+ if (match[2]) {
577
+ const binary = atob(payload);
578
+ const bytes = new Uint8Array(binary.length);
579
+ for (let index = 0; index < binary.length; index += 1) {
580
+ bytes[index] = binary.charCodeAt(index);
581
+ }
582
+ return { bytes, mimeType };
583
+ }
584
+ return {
585
+ bytes: new TextEncoder().encode(decodeURIComponent(payload)),
586
+ mimeType
587
+ };
588
+ }
589
+ function textureFieldSuffix(field) {
590
+ switch (field) {
591
+ case "baseColorTexture":
592
+ return "color";
593
+ case "metallicRoughnessTexture":
594
+ return "orm";
595
+ default:
596
+ return "normal";
597
+ }
598
+ }
599
+ function inferModelExtension(path, modelFormat) {
600
+ if (typeof modelFormat === "string" && modelFormat.length > 0) {
601
+ return modelFormat.toLowerCase();
602
+ }
603
+ return inferExtensionFromPath(path) ?? "bin";
604
+ }
605
+ function inferExtension(mimeType, fallback) {
606
+ const normalized = mimeType?.toLowerCase();
607
+ if (normalized === "image/png") {
608
+ return "png";
609
+ }
610
+ if (normalized === "image/jpeg") {
611
+ return "jpg";
612
+ }
613
+ if (normalized === "image/svg+xml") {
614
+ return "svg";
615
+ }
616
+ if (normalized === "image/vnd.radiance") {
617
+ return "hdr";
618
+ }
619
+ if (normalized === "model/gltf+json") {
620
+ return "gltf";
621
+ }
622
+ if (normalized === "model/gltf-binary" || normalized === "application/octet-stream") {
623
+ return fallback ?? "bin";
624
+ }
625
+ return fallback ?? "bin";
626
+ }
627
+ function inferExtensionFromPath(path) {
628
+ const cleanPath = path.split("?")[0]?.split("#")[0] ?? path;
629
+ const parts = cleanPath.split(".");
630
+ return parts.length > 1 ? parts.at(-1)?.toLowerCase() : void 0;
631
+ }
632
+ function ensureUniquePath(path, usedPaths) {
633
+ if (!usedPaths.has(path)) {
634
+ usedPaths.add(path);
635
+ return path;
636
+ }
637
+ const lastDot = path.lastIndexOf(".");
638
+ const stem = lastDot >= 0 ? path.slice(0, lastDot) : path;
639
+ const extension = lastDot >= 0 ? path.slice(lastDot) : "";
640
+ let counter = 2;
641
+ while (usedPaths.has(`${stem}-${counter}${extension}`)) {
642
+ counter += 1;
643
+ }
644
+ const resolved = `${stem}-${counter}${extension}`;
645
+ usedPaths.add(resolved);
646
+ return resolved;
647
+ }
648
+ function isDataUrl(value) {
649
+ return value.startsWith("data:");
650
+ }
651
+ function slugify(value) {
652
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
653
+ return normalized || "asset";
654
+ }
655
+ function trimSlashes(value) {
656
+ return value.replace(/^\/+|\/+$/g, "");
657
+ }
658
+
659
+ // ../runtime-build/src/snapshot-build.ts
660
+ import { MeshBVH } from "three-mesh-bvh";
661
+ import {
662
+ Box3,
663
+ BoxGeometry,
664
+ BufferGeometry,
665
+ ConeGeometry,
666
+ Float32BufferAttribute,
667
+ Group,
668
+ Mesh,
669
+ MeshStandardMaterial,
670
+ RepeatWrapping,
671
+ Scene,
672
+ SphereGeometry,
673
+ SRGBColorSpace,
674
+ TextureLoader,
675
+ Vector3 as Vector32,
676
+ CylinderGeometry
677
+ } from "three";
678
+ import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
679
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
680
+ import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader.js";
681
+ import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
682
+ var gltfLoader = new GLTFLoader();
683
+ var gltfExporter = new GLTFExporter();
684
+ var mtlLoader = new MTLLoader();
685
+ var modelTextureLoader = new TextureLoader();
686
+ async function buildRuntimeBundleFromSnapshot(snapshot, options = {}) {
687
+ return externalizeRuntimeAssets(await buildRuntimeSceneFromSnapshot(snapshot), options);
688
+ }
689
+ async function serializeRuntimeScene(snapshot) {
690
+ return JSON.stringify(await buildRuntimeSceneFromSnapshot(snapshot));
691
+ }
692
+ async function buildRuntimeSceneFromSnapshot(snapshot) {
693
+ const assetsById = new Map(snapshot.assets.map((asset) => [asset.id, asset]));
694
+ const materialsById = new Map(snapshot.materials.map((material) => [material.id, material]));
695
+ const exportedMaterials = await Promise.all(snapshot.materials.map((material) => resolveRuntimeMaterial(material)));
696
+ const shouldBakeLods = snapshot.settings.world.lod.enabled;
697
+ const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
698
+ const exportedSettings = shouldBakeLods ? {
699
+ ...snapshot.settings,
700
+ world: {
701
+ ...snapshot.settings.world,
702
+ lod: {
703
+ ...snapshot.settings.world.lod,
704
+ bakedAt: exportedAt
705
+ }
706
+ }
707
+ } : snapshot.settings;
708
+ const generatedAssets = [];
709
+ const exportedNodes = [];
710
+ for (const node of snapshot.nodes) {
711
+ if (isGroupNode(node)) {
712
+ exportedNodes.push({
713
+ data: node.data,
714
+ hooks: node.hooks,
715
+ id: node.id,
716
+ kind: "group",
717
+ metadata: node.metadata,
718
+ name: node.name,
719
+ parentId: node.parentId,
720
+ tags: node.tags,
721
+ transform: node.transform
722
+ });
723
+ continue;
724
+ }
725
+ if (isBrushNode(node)) {
726
+ const geometry = await buildExportGeometry(node, materialsById);
727
+ exportedNodes.push({
728
+ data: node.data,
729
+ geometry,
730
+ hooks: node.hooks,
731
+ id: node.id,
732
+ kind: "brush",
733
+ lods: shouldBakeLods ? await buildGeometryLods(geometry, snapshot.settings.world.lod) : void 0,
734
+ metadata: node.metadata,
735
+ name: node.name,
736
+ parentId: node.parentId,
737
+ tags: node.tags,
738
+ transform: node.transform
739
+ });
740
+ continue;
741
+ }
742
+ if (isMeshNode(node)) {
743
+ const geometry = await buildExportGeometry(node, materialsById);
744
+ exportedNodes.push({
745
+ data: node.data,
746
+ geometry,
747
+ hooks: node.hooks,
748
+ id: node.id,
749
+ kind: "mesh",
750
+ lods: shouldBakeLods ? await buildGeometryLods(geometry, snapshot.settings.world.lod) : void 0,
751
+ metadata: node.metadata,
752
+ name: node.name,
753
+ parentId: node.parentId,
754
+ tags: node.tags,
755
+ transform: node.transform
756
+ });
757
+ continue;
758
+ }
759
+ if (isPrimitiveNode(node)) {
760
+ const geometry = await buildExportGeometry(node, materialsById);
761
+ exportedNodes.push({
762
+ data: node.data,
763
+ geometry,
764
+ hooks: node.hooks,
765
+ id: node.id,
766
+ kind: "primitive",
767
+ lods: shouldBakeLods ? await buildGeometryLods(geometry, snapshot.settings.world.lod) : void 0,
768
+ metadata: node.metadata,
769
+ name: node.name,
770
+ parentId: node.parentId,
771
+ tags: node.tags,
772
+ transform: node.transform
773
+ });
774
+ continue;
775
+ }
776
+ if (isModelNode(node)) {
777
+ const modelLodBake = shouldBakeLods ? await buildModelLods(node.name, assetsById.get(node.data.assetId), node.id, snapshot.settings.world.lod) : void 0;
778
+ generatedAssets.push(...modelLodBake?.assets ?? []);
779
+ exportedNodes.push({
780
+ data: node.data,
781
+ hooks: node.hooks,
782
+ id: node.id,
783
+ kind: "model",
784
+ lods: modelLodBake?.lods,
785
+ metadata: node.metadata,
786
+ name: node.name,
787
+ parentId: node.parentId,
788
+ tags: node.tags,
789
+ transform: node.transform
790
+ });
791
+ continue;
792
+ }
793
+ if (isInstancingNode(node)) {
794
+ const sourceNode = resolveInstancingSourceNode(snapshot.nodes, node);
795
+ if (!sourceNode || !(isBrushNode(sourceNode) || isMeshNode(sourceNode) || isPrimitiveNode(sourceNode) || isModelNode(sourceNode))) {
796
+ continue;
797
+ }
798
+ exportedNodes.push({
799
+ data: {
800
+ sourceNodeId: sourceNode.id
801
+ },
802
+ hooks: node.hooks,
803
+ id: node.id,
804
+ kind: "instancing",
805
+ metadata: node.metadata,
806
+ name: node.name,
807
+ parentId: node.parentId,
808
+ tags: node.tags,
809
+ transform: sanitizeInstanceTransform(node.transform)
810
+ });
811
+ continue;
812
+ }
813
+ exportedNodes.push({
814
+ data: node.data,
815
+ id: node.id,
816
+ kind: "light",
817
+ metadata: node.metadata,
818
+ name: node.name,
819
+ parentId: node.parentId,
820
+ tags: node.tags,
821
+ transform: node.transform
822
+ });
823
+ }
824
+ return {
825
+ assets: [...snapshot.assets, ...generatedAssets],
826
+ entities: snapshot.entities,
827
+ layers: snapshot.layers,
828
+ materials: exportedMaterials,
829
+ metadata: {
830
+ exportedAt,
831
+ format: "web-hammer-engine",
832
+ version: CURRENT_RUNTIME_SCENE_VERSION
833
+ },
834
+ nodes: exportedNodes,
835
+ settings: exportedSettings
836
+ };
837
+ }
838
+ async function buildExportGeometry(node, materialsById) {
839
+ const fallbackMaterial = await resolveRuntimeMaterial({
840
+ color: node.kind === "brush" ? "#f69036" : node.kind === "primitive" && node.data.role === "prop" ? "#7f8ea3" : "#6ed5c0",
841
+ id: `material:fallback:${node.id}`,
842
+ metalness: node.kind === "brush" ? 0 : node.kind === "primitive" && node.data.role === "prop" ? 0.12 : 0.05,
843
+ name: `${node.name} Default`,
844
+ roughness: node.kind === "brush" ? 0.95 : node.kind === "primitive" && node.data.role === "prop" ? 0.64 : 0.82
845
+ });
846
+ const primitiveByMaterial = /* @__PURE__ */ new Map();
847
+ const appendFace = async (params) => {
848
+ const material = params.faceMaterialId ? await resolveRuntimeMaterial(materialsById.get(params.faceMaterialId)) : fallbackMaterial;
849
+ const primitive = primitiveByMaterial.get(material.id) ?? {
850
+ indices: [],
851
+ material,
852
+ normals: [],
853
+ positions: [],
854
+ uvs: []
855
+ };
856
+ const vertexOffset = primitive.positions.length / 3;
857
+ 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);
858
+ params.vertices.forEach((vertex) => {
859
+ primitive.positions.push(vertex.x, vertex.y, vertex.z);
860
+ primitive.normals.push(params.normal.x, params.normal.y, params.normal.z);
861
+ });
862
+ primitive.uvs.push(...uvs);
863
+ params.triangleIndices.forEach((index) => {
864
+ primitive.indices.push(vertexOffset + index);
865
+ });
866
+ primitiveByMaterial.set(material.id, primitive);
867
+ };
868
+ if (isBrushNode(node)) {
869
+ const rebuilt = reconstructBrushFaces(node.data);
870
+ if (!rebuilt.valid) {
871
+ return { primitives: [] };
872
+ }
873
+ for (const face of rebuilt.faces) {
874
+ await appendFace({
875
+ faceMaterialId: face.materialId,
876
+ normal: face.normal,
877
+ triangleIndices: face.triangleIndices,
878
+ uvOffset: face.uvOffset,
879
+ uvScale: face.uvScale,
880
+ vertices: face.vertices.map((vertex) => vertex.position)
881
+ });
882
+ }
883
+ }
884
+ if (isMeshNode(node)) {
885
+ for (const face of node.data.faces) {
886
+ const triangulated = triangulateMeshFace(node.data, face.id);
887
+ if (!triangulated) {
888
+ continue;
889
+ }
890
+ await appendFace({
891
+ faceMaterialId: face.materialId,
892
+ normal: triangulated.normal,
893
+ triangleIndices: triangulated.indices,
894
+ uvOffset: face.uvOffset,
895
+ uvScale: face.uvScale,
896
+ uvs: face.uvs,
897
+ vertices: getFaceVertices(node.data, face.id).map((vertex) => vertex.position)
898
+ });
899
+ }
900
+ }
901
+ if (isPrimitiveNode(node)) {
902
+ const material = node.data.materialId ? await resolveRuntimeMaterial(materialsById.get(node.data.materialId)) : fallbackMaterial;
903
+ const primitive = buildPrimitiveGeometry(node.data.shape, node.data.size, node.data.radialSegments ?? 24);
904
+ if (primitive) {
905
+ primitiveByMaterial.set(material.id, {
906
+ indices: primitive.indices,
907
+ material,
908
+ normals: primitive.normals,
909
+ positions: primitive.positions,
910
+ uvs: primitive.uvs
911
+ });
912
+ }
913
+ }
914
+ return {
915
+ primitives: Array.from(primitiveByMaterial.values())
916
+ };
917
+ }
918
+ async function buildGeometryLods(geometry, settings) {
919
+ if (!geometry.primitives.length) {
920
+ return void 0;
921
+ }
922
+ const midGeometry = simplifyExportGeometry(geometry, settings.midDetailRatio);
923
+ const lowGeometry = simplifyExportGeometry(geometry, settings.lowDetailRatio);
924
+ const lods = [];
925
+ if (midGeometry) {
926
+ lods.push({
927
+ geometry: midGeometry,
928
+ level: "mid"
929
+ });
930
+ }
931
+ if (lowGeometry) {
932
+ lods.push({
933
+ geometry: lowGeometry,
934
+ level: "low"
935
+ });
936
+ }
937
+ return lods.length ? lods : void 0;
938
+ }
939
+ async function buildModelLods(name, asset, nodeId, settings) {
940
+ if (!asset?.path) {
941
+ return { assets: [], lods: void 0 };
942
+ }
943
+ const source = await loadModelSceneForLodBake(asset);
944
+ const bakedLevels = [];
945
+ for (const [level, ratio] of [
946
+ ["mid", settings.midDetailRatio],
947
+ ["low", settings.lowDetailRatio]
948
+ ]) {
949
+ const simplified = simplifyModelSceneForRatio(source, ratio);
950
+ if (!simplified) {
951
+ continue;
952
+ }
953
+ const bytes = await exportModelSceneAsGlb(simplified);
954
+ bakedLevels.push({
955
+ asset: createGeneratedModelLodAsset(asset, name, nodeId, level, bytes),
956
+ level
957
+ });
958
+ }
959
+ return {
960
+ assets: bakedLevels.map((entry) => entry.asset),
961
+ lods: bakedLevels.length ? bakedLevels.map((entry) => ({
962
+ assetId: entry.asset.id,
963
+ level: entry.level
964
+ })) : void 0
965
+ };
966
+ }
967
+ async function loadModelSceneForLodBake(asset) {
968
+ const format = resolveModelAssetFormat(asset);
969
+ if (format === "obj") {
970
+ const objLoader = new OBJLoader();
971
+ const texturePath = readModelAssetString(asset, "texturePath");
972
+ const resolvedTexturePath = typeof texturePath === "string" && texturePath.length > 0 ? texturePath : void 0;
973
+ const mtlText = readModelAssetString(asset, "materialMtlText");
974
+ if (mtlText) {
975
+ const materialCreator = mtlLoader.parse(patchMtlTextureReferences(mtlText, resolvedTexturePath), "");
976
+ materialCreator.preload();
977
+ objLoader.setMaterials(materialCreator);
978
+ } else {
979
+ objLoader.setMaterials(void 0);
980
+ }
981
+ const object = await objLoader.loadAsync(asset.path);
982
+ if (!mtlText && resolvedTexturePath) {
983
+ const texture = await modelTextureLoader.loadAsync(resolvedTexturePath);
984
+ texture.wrapS = RepeatWrapping;
985
+ texture.wrapT = RepeatWrapping;
986
+ texture.colorSpace = SRGBColorSpace;
987
+ object.traverse((child) => {
988
+ if (child instanceof Mesh) {
989
+ child.material = new MeshStandardMaterial({
990
+ map: texture,
991
+ metalness: 0.12,
992
+ roughness: 0.76
993
+ });
994
+ }
995
+ });
996
+ }
997
+ return object;
998
+ }
999
+ return (await gltfLoader.loadAsync(asset.path)).scene;
1000
+ }
1001
+ function simplifyModelSceneForRatio(source, ratio) {
1002
+ if (ratio >= 0.98) {
1003
+ return void 0;
1004
+ }
1005
+ const simplifiedRoot = source.clone(true);
1006
+ expandGroupedModelMeshesForLodBake(simplifiedRoot);
1007
+ let simplifiedMeshCount = 0;
1008
+ simplifiedRoot.traverse((child) => {
1009
+ if (!(child instanceof Mesh)) {
1010
+ return;
1011
+ }
1012
+ if ("isSkinnedMesh" in child && child.isSkinnedMesh) {
1013
+ return;
1014
+ }
1015
+ const simplifiedGeometry = simplifyModelGeometry(child.geometry, ratio);
1016
+ if (!simplifiedGeometry) {
1017
+ return;
1018
+ }
1019
+ child.geometry = simplifiedGeometry;
1020
+ simplifiedMeshCount += 1;
1021
+ });
1022
+ return simplifiedMeshCount > 0 ? simplifiedRoot : void 0;
1023
+ }
1024
+ function expandGroupedModelMeshesForLodBake(root) {
1025
+ const replacements = [];
1026
+ root.traverse((child) => {
1027
+ if (!(child instanceof Mesh) || !Array.isArray(child.material) || child.geometry.groups.length <= 1 || !child.parent) {
1028
+ return;
1029
+ }
1030
+ const container = new Group();
1031
+ container.name = child.name ? `${child.name}:lod-groups` : "lod-groups";
1032
+ container.position.copy(child.position);
1033
+ container.quaternion.copy(child.quaternion);
1034
+ container.scale.copy(child.scale);
1035
+ container.visible = child.visible;
1036
+ container.renderOrder = child.renderOrder;
1037
+ container.userData = structuredClone(child.userData ?? {});
1038
+ child.geometry.groups.forEach((group, groupIndex) => {
1039
+ const material = child.material[group.materialIndex] ?? child.material[0];
1040
+ if (!material) {
1041
+ return;
1042
+ }
1043
+ const partGeometry = extractGeometryGroup(child.geometry, group.start, group.count);
1044
+ const partMesh = new Mesh(partGeometry, material);
1045
+ partMesh.name = child.name ? `${child.name}:group:${groupIndex}` : `group:${groupIndex}`;
1046
+ partMesh.castShadow = child.castShadow;
1047
+ partMesh.receiveShadow = child.receiveShadow;
1048
+ partMesh.userData = structuredClone(child.userData ?? {});
1049
+ container.add(partMesh);
1050
+ });
1051
+ replacements.push({
1052
+ container,
1053
+ mesh: child,
1054
+ parent: child.parent
1055
+ });
1056
+ });
1057
+ replacements.forEach(({ container, mesh, parent }) => {
1058
+ parent.add(container);
1059
+ parent.remove(mesh);
1060
+ });
1061
+ }
1062
+ function extractGeometryGroup(geometry, start, count) {
1063
+ const groupGeometry = new BufferGeometry();
1064
+ const index = geometry.getIndex();
1065
+ const attributes = geometry.attributes;
1066
+ Object.entries(attributes).forEach(([name, attribute]) => {
1067
+ groupGeometry.setAttribute(name, attribute);
1068
+ });
1069
+ if (index) {
1070
+ groupGeometry.setIndex(Array.from(index.array).slice(start, start + count));
1071
+ } else {
1072
+ groupGeometry.setIndex(Array.from({ length: count }, (_, offset) => start + offset));
1073
+ }
1074
+ groupGeometry.computeBoundingBox();
1075
+ groupGeometry.computeBoundingSphere();
1076
+ return groupGeometry;
1077
+ }
1078
+ function simplifyModelGeometry(geometry, ratio) {
1079
+ const positionAttribute = geometry.getAttribute("position");
1080
+ const vertexCount = positionAttribute?.count ?? 0;
1081
+ if (!positionAttribute || vertexCount < 12 || ratio >= 0.98) {
1082
+ return void 0;
1083
+ }
1084
+ const workingGeometry = geometry.getAttribute("normal") ? geometry : geometry.clone();
1085
+ if (!workingGeometry.getAttribute("normal")) {
1086
+ workingGeometry.computeVertexNormals();
1087
+ }
1088
+ workingGeometry.computeBoundingBox();
1089
+ const bounds = workingGeometry.boundingBox?.clone();
1090
+ if (!bounds) {
1091
+ if (workingGeometry !== geometry) {
1092
+ workingGeometry.dispose();
1093
+ }
1094
+ return void 0;
1095
+ }
1096
+ const normalAttribute = workingGeometry.getAttribute("normal");
1097
+ const uvAttribute = workingGeometry.getAttribute("uv");
1098
+ const index = workingGeometry.getIndex();
1099
+ const simplified = simplifyPrimitiveWithVertexClustering(
1100
+ {
1101
+ indices: index ? Array.from(index.array) : Array.from({ length: vertexCount }, (_, value) => value),
1102
+ material: {
1103
+ color: "#ffffff",
1104
+ id: "material:model-simplify",
1105
+ metallicFactor: 0,
1106
+ name: "Model Simplify",
1107
+ roughnessFactor: 1
1108
+ },
1109
+ normals: Array.from(normalAttribute.array),
1110
+ positions: Array.from(positionAttribute.array),
1111
+ uvs: uvAttribute ? Array.from(uvAttribute.array) : []
1112
+ },
1113
+ ratio,
1114
+ bounds
1115
+ );
1116
+ if (workingGeometry !== geometry) {
1117
+ workingGeometry.dispose();
1118
+ }
1119
+ if (!simplified) {
1120
+ return void 0;
1121
+ }
1122
+ const simplifiedGeometry = createBufferGeometryFromPrimitive(simplified);
1123
+ simplifiedGeometry.computeBoundingBox();
1124
+ simplifiedGeometry.computeBoundingSphere();
1125
+ return simplifiedGeometry;
1126
+ }
1127
+ async function exportModelSceneAsGlb(object) {
1128
+ try {
1129
+ return await exportGlbBytesFromObject(object);
1130
+ } catch {
1131
+ return await exportGlbBytesFromObject(stripTextureReferencesFromObject(object.clone(true)));
1132
+ }
1133
+ }
1134
+ async function exportGlbBytesFromObject(object) {
1135
+ const scene = new Scene();
1136
+ scene.add(object);
1137
+ const exported = await gltfExporter.parseAsync(scene, {
1138
+ binary: true,
1139
+ includeCustomExtensions: false
1140
+ });
1141
+ if (!(exported instanceof ArrayBuffer)) {
1142
+ throw new Error("Expected GLB binary output for baked model LOD.");
1143
+ }
1144
+ return new Uint8Array(exported);
1145
+ }
1146
+ function stripTextureReferencesFromObject(object) {
1147
+ object.traverse((child) => {
1148
+ if (!(child instanceof Mesh)) {
1149
+ return;
1150
+ }
1151
+ const strip = (material) => {
1152
+ const clone = material.clone();
1153
+ clone.alphaMap = null;
1154
+ clone.aoMap = null;
1155
+ clone.bumpMap = null;
1156
+ clone.displacementMap = null;
1157
+ clone.emissiveMap = null;
1158
+ clone.lightMap = null;
1159
+ clone.map = null;
1160
+ clone.metalnessMap = null;
1161
+ clone.normalMap = null;
1162
+ clone.roughnessMap = null;
1163
+ return clone;
1164
+ };
1165
+ if (Array.isArray(child.material)) {
1166
+ child.material = child.material.map(
1167
+ (material) => material instanceof MeshStandardMaterial ? strip(material) : new MeshStandardMaterial({
1168
+ color: "color" in material ? material.color : "#7f8ea3",
1169
+ metalness: "metalness" in material && typeof material.metalness === "number" ? material.metalness : 0.1,
1170
+ roughness: "roughness" in material && typeof material.roughness === "number" ? material.roughness : 0.8
1171
+ })
1172
+ );
1173
+ return;
1174
+ }
1175
+ const fallbackMaterial = child.material;
1176
+ child.material = child.material instanceof MeshStandardMaterial ? strip(child.material) : new MeshStandardMaterial({
1177
+ color: "color" in fallbackMaterial ? fallbackMaterial.color : "#7f8ea3",
1178
+ metalness: "metalness" in fallbackMaterial && typeof fallbackMaterial.metalness === "number" ? fallbackMaterial.metalness : 0.1,
1179
+ roughness: "roughness" in fallbackMaterial && typeof fallbackMaterial.roughness === "number" ? fallbackMaterial.roughness : 0.8
1180
+ });
1181
+ });
1182
+ return object;
1183
+ }
1184
+ function createGeneratedModelLodAsset(asset, name, nodeId, level, bytes) {
1185
+ return {
1186
+ id: `asset:model-lod:${slugify2(`${name}-${nodeId}`)}:${level}`,
1187
+ metadata: {
1188
+ ...asset.metadata,
1189
+ lodGenerated: true,
1190
+ lodLevel: level,
1191
+ lodSourceAssetId: asset.id,
1192
+ materialMtlText: "",
1193
+ modelFormat: "glb",
1194
+ texturePath: ""
1195
+ },
1196
+ path: createBinaryDataUrl(bytes, "model/gltf-binary"),
1197
+ type: "model"
1198
+ };
1199
+ }
1200
+ function createBinaryDataUrl(bytes, mimeType) {
1201
+ let binary = "";
1202
+ const chunkSize = 32768;
1203
+ for (let index = 0; index < bytes.length; index += chunkSize) {
1204
+ binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
1205
+ }
1206
+ return `data:${mimeType};base64,${btoa(binary)}`;
1207
+ }
1208
+ function sanitizeInstanceTransform(transform) {
1209
+ return {
1210
+ position: structuredClone(transform.position),
1211
+ rotation: structuredClone(transform.rotation),
1212
+ scale: structuredClone(transform.scale)
1213
+ };
1214
+ }
1215
+ function resolveModelAssetFormat(asset) {
1216
+ const format = readModelAssetString(asset, "modelFormat")?.toLowerCase();
1217
+ return format === "obj" || asset.path.toLowerCase().endsWith(".obj") ? "obj" : "gltf";
1218
+ }
1219
+ function readModelAssetString(asset, key) {
1220
+ const value = asset?.metadata[key];
1221
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1222
+ }
1223
+ function patchMtlTextureReferences(mtlText, texturePath) {
1224
+ if (!texturePath) {
1225
+ return mtlText;
1226
+ }
1227
+ const mapPattern = /^(map_Ka|map_Kd|map_d|map_Bump|bump)\s+.+$/gm;
1228
+ const hasDiffuseMap = /^map_Kd\s+.+$/m.test(mtlText);
1229
+ const normalized = mtlText.replace(mapPattern, (line) => {
1230
+ if (line.startsWith("map_Kd ")) {
1231
+ return `map_Kd ${texturePath}`;
1232
+ }
1233
+ return line;
1234
+ });
1235
+ return hasDiffuseMap ? normalized : `${normalized.trim()}
1236
+ map_Kd ${texturePath}
1237
+ `;
1238
+ }
1239
+ function slugify2(value) {
1240
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1241
+ return normalized || "model";
1242
+ }
1243
+ function simplifyExportGeometry(geometry, ratio) {
1244
+ const primitives = geometry.primitives.map((primitive) => simplifyExportPrimitive(primitive, ratio)).filter((primitive) => primitive !== void 0);
1245
+ return primitives.length ? { primitives } : void 0;
1246
+ }
1247
+ function simplifyExportPrimitive(primitive, ratio) {
1248
+ const vertexCount = Math.floor(primitive.positions.length / 3);
1249
+ const triangleCount = Math.floor(primitive.indices.length / 3);
1250
+ if (vertexCount < 12 || triangleCount < 8 || ratio >= 0.98) {
1251
+ return void 0;
1252
+ }
1253
+ const geometry = createBufferGeometryFromPrimitive(primitive);
1254
+ const boundsTree = new MeshBVH(geometry, { maxLeafSize: 12, setBoundingBox: true });
1255
+ const bounds = boundsTree.getBoundingBox(new Box3());
1256
+ const simplified = simplifyPrimitiveWithVertexClustering(primitive, ratio, bounds);
1257
+ geometry.dispose();
1258
+ if (!simplified) {
1259
+ return void 0;
1260
+ }
1261
+ return simplified;
1262
+ }
1263
+ function createBufferGeometryFromPrimitive(primitive) {
1264
+ const geometry = new BufferGeometry();
1265
+ geometry.setAttribute("position", new Float32BufferAttribute(primitive.positions, 3));
1266
+ geometry.setAttribute("normal", new Float32BufferAttribute(primitive.normals, 3));
1267
+ if (primitive.uvs.length) {
1268
+ geometry.setAttribute("uv", new Float32BufferAttribute(primitive.uvs, 2));
1269
+ }
1270
+ geometry.setIndex(primitive.indices);
1271
+ return geometry;
1272
+ }
1273
+ function simplifyPrimitiveWithVertexClustering(primitive, ratio, bounds) {
1274
+ const targetVertexCount = Math.max(8, Math.floor(primitive.positions.length / 3 * Math.max(0.04, ratio)));
1275
+ const size = bounds.getSize(new Vector32());
1276
+ let resolution = Math.max(1, Math.round(Math.cbrt(targetVertexCount)));
1277
+ let best;
1278
+ for (let attempt = 0; attempt < 5; attempt += 1) {
1279
+ const simplified = clusterPrimitiveVertices(primitive, bounds, size, Math.max(1, resolution - attempt));
1280
+ if (!simplified) {
1281
+ continue;
1282
+ }
1283
+ best = simplified;
1284
+ if (simplified.positions.length / 3 <= targetVertexCount) {
1285
+ break;
1286
+ }
1287
+ }
1288
+ if (!best) {
1289
+ return void 0;
1290
+ }
1291
+ if (best.positions.length >= primitive.positions.length || best.indices.length >= primitive.indices.length) {
1292
+ return void 0;
1293
+ }
1294
+ return best;
1295
+ }
1296
+ function clusterPrimitiveVertices(primitive, bounds, size, resolution) {
1297
+ const min = bounds.min;
1298
+ const cellSizeX = Math.max(size.x / resolution, 1e-4);
1299
+ const cellSizeY = Math.max(size.y / resolution, 1e-4);
1300
+ const cellSizeZ = Math.max(size.z / resolution, 1e-4);
1301
+ const vertexCount = primitive.positions.length / 3;
1302
+ const clusters = /* @__PURE__ */ new Map();
1303
+ const clusterKeyByVertex = new Array(vertexCount);
1304
+ for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex += 1) {
1305
+ const positionOffset = vertexIndex * 3;
1306
+ const uvOffset = vertexIndex * 2;
1307
+ const x = primitive.positions[positionOffset];
1308
+ const y = primitive.positions[positionOffset + 1];
1309
+ const z = primitive.positions[positionOffset + 2];
1310
+ const normalX = primitive.normals[positionOffset];
1311
+ const normalY = primitive.normals[positionOffset + 1];
1312
+ const normalZ = primitive.normals[positionOffset + 2];
1313
+ const cellX = Math.floor((x - min.x) / cellSizeX);
1314
+ const cellY = Math.floor((y - min.y) / cellSizeY);
1315
+ const cellZ = Math.floor((z - min.z) / cellSizeZ);
1316
+ const clusterKey = `${cellX}:${cellY}:${cellZ}:${resolveNormalBucket(normalX, normalY, normalZ)}`;
1317
+ const cluster = clusters.get(clusterKey) ?? {
1318
+ count: 0,
1319
+ normalX: 0,
1320
+ normalY: 0,
1321
+ normalZ: 0,
1322
+ positionX: 0,
1323
+ positionY: 0,
1324
+ positionZ: 0,
1325
+ uvX: 0,
1326
+ uvY: 0
1327
+ };
1328
+ cluster.count += 1;
1329
+ cluster.positionX += x;
1330
+ cluster.positionY += y;
1331
+ cluster.positionZ += z;
1332
+ cluster.normalX += normalX;
1333
+ cluster.normalY += normalY;
1334
+ cluster.normalZ += normalZ;
1335
+ cluster.uvX += primitive.uvs[uvOffset] ?? 0;
1336
+ cluster.uvY += primitive.uvs[uvOffset + 1] ?? 0;
1337
+ clusters.set(clusterKey, cluster);
1338
+ clusterKeyByVertex[vertexIndex] = clusterKey;
1339
+ }
1340
+ const remappedIndices = [];
1341
+ const positions = [];
1342
+ const normals = [];
1343
+ const uvs = [];
1344
+ const clusterIndexByKey = /* @__PURE__ */ new Map();
1345
+ const ensureClusterIndex = (clusterKey) => {
1346
+ const existing = clusterIndexByKey.get(clusterKey);
1347
+ if (existing !== void 0) {
1348
+ return existing;
1349
+ }
1350
+ const cluster = clusters.get(clusterKey);
1351
+ if (!cluster || cluster.count === 0) {
1352
+ return void 0;
1353
+ }
1354
+ const averagedNormal = normalizeVec3(vec3(cluster.normalX, cluster.normalY, cluster.normalZ));
1355
+ const index = positions.length / 3;
1356
+ positions.push(cluster.positionX / cluster.count, cluster.positionY / cluster.count, cluster.positionZ / cluster.count);
1357
+ normals.push(averagedNormal.x, averagedNormal.y, averagedNormal.z);
1358
+ uvs.push(cluster.uvX / cluster.count, cluster.uvY / cluster.count);
1359
+ clusterIndexByKey.set(clusterKey, index);
1360
+ return index;
1361
+ };
1362
+ for (let index = 0; index < primitive.indices.length; index += 3) {
1363
+ const a = ensureClusterIndex(clusterKeyByVertex[primitive.indices[index]]);
1364
+ const b = ensureClusterIndex(clusterKeyByVertex[primitive.indices[index + 1]]);
1365
+ const c = ensureClusterIndex(clusterKeyByVertex[primitive.indices[index + 2]]);
1366
+ if (a === void 0 || b === void 0 || c === void 0) {
1367
+ continue;
1368
+ }
1369
+ if (a === b || b === c || a === c) {
1370
+ continue;
1371
+ }
1372
+ if (triangleArea(positions, a, b, c) <= 1e-6) {
1373
+ continue;
1374
+ }
1375
+ remappedIndices.push(a, b, c);
1376
+ }
1377
+ if (remappedIndices.length < 12 || positions.length >= primitive.positions.length) {
1378
+ return void 0;
1379
+ }
1380
+ return {
1381
+ indices: remappedIndices,
1382
+ material: primitive.material,
1383
+ normals,
1384
+ positions,
1385
+ uvs
1386
+ };
1387
+ }
1388
+ function resolveNormalBucket(x, y, z) {
1389
+ const ax = Math.abs(x);
1390
+ const ay = Math.abs(y);
1391
+ const az = Math.abs(z);
1392
+ if (ax >= ay && ax >= az) {
1393
+ return x >= 0 ? "xp" : "xn";
1394
+ }
1395
+ if (ay >= ax && ay >= az) {
1396
+ return y >= 0 ? "yp" : "yn";
1397
+ }
1398
+ return z >= 0 ? "zp" : "zn";
1399
+ }
1400
+ function triangleArea(positions, a, b, c) {
1401
+ const ax = positions[a * 3];
1402
+ const ay = positions[a * 3 + 1];
1403
+ const az = positions[a * 3 + 2];
1404
+ const bx = positions[b * 3];
1405
+ const by = positions[b * 3 + 1];
1406
+ const bz = positions[b * 3 + 2];
1407
+ const cx = positions[c * 3];
1408
+ const cy = positions[c * 3 + 1];
1409
+ const cz = positions[c * 3 + 2];
1410
+ const ab = vec3(bx - ax, by - ay, bz - az);
1411
+ const ac = vec3(cx - ax, cy - ay, cz - az);
1412
+ const cross = crossVec3(ab, ac);
1413
+ return Math.sqrt(cross.x * cross.x + cross.y * cross.y + cross.z * cross.z) * 0.5;
1414
+ }
1415
+ function buildPrimitiveGeometry(shape, size, radialSegments) {
1416
+ const geometry = shape === "cube" ? new BoxGeometry(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);
1417
+ const positionAttribute = geometry.getAttribute("position");
1418
+ const normalAttribute = geometry.getAttribute("normal");
1419
+ const uvAttribute = geometry.getAttribute("uv");
1420
+ const index = geometry.getIndex();
1421
+ const primitive = {
1422
+ indices: index ? Array.from(index.array) : Array.from({ length: positionAttribute.count }, (_, value) => value),
1423
+ normals: Array.from(normalAttribute.array),
1424
+ positions: Array.from(positionAttribute.array),
1425
+ uvs: uvAttribute ? Array.from(uvAttribute.array) : []
1426
+ };
1427
+ geometry.dispose();
1428
+ return primitive;
1429
+ }
1430
+ async function resolveRuntimeMaterial(material) {
1431
+ const resolved = material ?? {
1432
+ color: "#ffffff",
1433
+ id: "material:fallback:default",
1434
+ metalness: 0.05,
1435
+ name: "Default Material",
1436
+ roughness: 0.8
1437
+ };
1438
+ return {
1439
+ baseColorTexture: await resolveEmbeddedTextureUri(resolved.colorTexture ?? resolveGeneratedBlockoutTexture(resolved)),
1440
+ color: resolved.color,
1441
+ id: resolved.id,
1442
+ metallicFactor: resolved.metalness ?? 0,
1443
+ metallicRoughnessTexture: await createMetallicRoughnessTextureDataUri(
1444
+ resolved.metalnessTexture,
1445
+ resolved.roughnessTexture,
1446
+ resolved.metalness ?? 0,
1447
+ resolved.roughness ?? 0.8
1448
+ ),
1449
+ name: resolved.name,
1450
+ normalTexture: await resolveEmbeddedTextureUri(resolved.normalTexture),
1451
+ roughnessFactor: resolved.roughness ?? 0.8,
1452
+ side: resolved.side
1453
+ };
1454
+ }
1455
+ function resolveGeneratedBlockoutTexture(material) {
1456
+ return material.category === "blockout" ? createBlockoutTextureDataUri(material.color, material.edgeColor ?? "#2f3540", material.edgeThickness ?? 0.035) : void 0;
1457
+ }
1458
+ function projectPlanarUvs(vertices, normal, uvScale, uvOffset) {
1459
+ const basis = createFacePlaneBasis(normal);
1460
+ const origin = vertices[0] ?? vec3(0, 0, 0);
1461
+ const scaleX = Math.abs(uvScale?.x ?? 1) <= 1e-4 ? 1 : uvScale?.x ?? 1;
1462
+ const scaleY = Math.abs(uvScale?.y ?? 1) <= 1e-4 ? 1 : uvScale?.y ?? 1;
1463
+ const offsetX = uvOffset?.x ?? 0;
1464
+ const offsetY = uvOffset?.y ?? 0;
1465
+ return vertices.flatMap((vertex) => {
1466
+ const offset = subVec3(vertex, origin);
1467
+ return [dotVec3(offset, basis.u) * scaleX + offsetX, dotVec3(offset, basis.v) * scaleY + offsetY];
1468
+ });
1469
+ }
1470
+ function createFacePlaneBasis(normal) {
1471
+ const normalizedNormal = normalizeVec3(normal);
1472
+ const reference = Math.abs(normalizedNormal.y) < 0.99 ? vec3(0, 1, 0) : vec3(1, 0, 0);
1473
+ const u = normalizeVec3(crossVec3(reference, normalizedNormal));
1474
+ const v = normalizeVec3(crossVec3(normalizedNormal, u));
1475
+ return { u, v };
1476
+ }
1477
+ async function resolveEmbeddedTextureUri(source) {
1478
+ if (!source) {
1479
+ return void 0;
1480
+ }
1481
+ if (source.startsWith("data:")) {
1482
+ return source;
1483
+ }
1484
+ const response = await fetch(source);
1485
+ const blob = await response.blob();
1486
+ const buffer = await blob.arrayBuffer();
1487
+ return `data:${blob.type || "application/octet-stream"};base64,${toBase64(new Uint8Array(buffer))}`;
1488
+ }
1489
+ async function createMetallicRoughnessTextureDataUri(metalnessSource, roughnessSource, metalnessFactor, roughnessFactor) {
1490
+ if (!metalnessSource && !roughnessSource) {
1491
+ return void 0;
1492
+ }
1493
+ if (typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {
1494
+ return void 0;
1495
+ }
1496
+ const [metalness, roughness] = await Promise.all([
1497
+ loadImagePixels(metalnessSource),
1498
+ loadImagePixels(roughnessSource)
1499
+ ]);
1500
+ const width = Math.max(metalness?.width ?? 1, roughness?.width ?? 1);
1501
+ const height = Math.max(metalness?.height ?? 1, roughness?.height ?? 1);
1502
+ const canvas = new OffscreenCanvas(width, height);
1503
+ const context = canvas.getContext("2d");
1504
+ if (!context) {
1505
+ return void 0;
1506
+ }
1507
+ const imageData = context.createImageData(width, height);
1508
+ const metalDefault = Math.round(clamp01(metalnessFactor) * 255);
1509
+ const roughDefault = Math.round(clamp01(roughnessFactor) * 255);
1510
+ for (let index = 0; index < imageData.data.length; index += 4) {
1511
+ imageData.data[index] = 0;
1512
+ imageData.data[index + 1] = roughness?.pixels[index] ?? roughDefault;
1513
+ imageData.data[index + 2] = metalness?.pixels[index] ?? metalDefault;
1514
+ imageData.data[index + 3] = 255;
1515
+ }
1516
+ context.putImageData(imageData, 0, 0);
1517
+ const blob = await canvas.convertToBlob({ type: "image/png" });
1518
+ const buffer = await blob.arrayBuffer();
1519
+ return `data:image/png;base64,${toBase64(new Uint8Array(buffer))}`;
1520
+ }
1521
+ async function loadImagePixels(source) {
1522
+ if (!source || typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {
1523
+ return void 0;
1524
+ }
1525
+ const response = await fetch(source);
1526
+ const blob = await response.blob();
1527
+ const bitmap = await createImageBitmap(blob);
1528
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
1529
+ const context = canvas.getContext("2d", { willReadFrequently: true });
1530
+ if (!context) {
1531
+ bitmap.close();
1532
+ return void 0;
1533
+ }
1534
+ context.drawImage(bitmap, 0, 0);
1535
+ bitmap.close();
1536
+ const imageData = context.getImageData(0, 0, bitmap.width, bitmap.height);
1537
+ return {
1538
+ height: imageData.height,
1539
+ pixels: imageData.data,
1540
+ width: imageData.width
1541
+ };
1542
+ }
1543
+ function clamp01(value) {
1544
+ return Math.max(0, Math.min(1, value));
1545
+ }
1546
+ function toBase64(bytes) {
1547
+ let binary = "";
1548
+ bytes.forEach((byte) => {
1549
+ binary += String.fromCharCode(byte);
1550
+ });
1551
+ return btoa(binary);
1552
+ }
1553
+
1554
+ // src/export-tasks.ts
1555
+ import { MeshBVH as MeshBVH2 } from "three-mesh-bvh";
1556
+ import {
1557
+ Box3 as Box32,
1558
+ BoxGeometry as BoxGeometry2,
1559
+ BufferGeometry as BufferGeometry2,
1560
+ ConeGeometry as ConeGeometry2,
1561
+ CylinderGeometry as CylinderGeometry2,
1562
+ Euler as Euler2,
1563
+ Float32BufferAttribute as Float32BufferAttribute2,
1564
+ Group as Group2,
1565
+ Mesh as Mesh2,
1566
+ MeshStandardMaterial as MeshStandardMaterial2,
1567
+ Quaternion as Quaternion2,
1568
+ RepeatWrapping as RepeatWrapping2,
1569
+ Scene as Scene2,
1570
+ SphereGeometry as SphereGeometry2,
1571
+ SRGBColorSpace as SRGBColorSpace2,
1572
+ TextureLoader as TextureLoader2,
1573
+ Vector3 as Vector33
1574
+ } from "three";
1575
+ import { GLTFExporter as GLTFExporter2 } from "three/examples/jsm/exporters/GLTFExporter.js";
1576
+ import { GLTFLoader as GLTFLoader2 } from "three/examples/jsm/loaders/GLTFLoader.js";
1577
+ import { MTLLoader as MTLLoader2 } from "three/examples/jsm/loaders/MTLLoader.js";
1578
+ import { OBJLoader as OBJLoader2 } from "three/examples/jsm/loaders/OBJLoader.js";
1579
+ var gltfLoader2 = new GLTFLoader2();
1580
+ var gltfExporter2 = new GLTFExporter2();
1581
+ var mtlLoader2 = new MTLLoader2();
1582
+ var modelTextureLoader2 = new TextureLoader2();
1583
+ async function executeWorkerRequest(request) {
1584
+ try {
1585
+ if (request.kind === "whmap-save") {
1586
+ return {
1587
+ id: request.id,
1588
+ kind: request.kind,
1589
+ ok: true,
1590
+ payload: serializeWhmap(request.snapshot)
1591
+ };
1592
+ }
1593
+ if (request.kind === "whmap-load") {
1594
+ return {
1595
+ id: request.id,
1596
+ kind: request.kind,
1597
+ ok: true,
1598
+ payload: parseWhmap(request.text)
1599
+ };
1600
+ }
1601
+ if (request.kind === "engine-export") {
1602
+ return {
1603
+ id: request.id,
1604
+ kind: request.kind,
1605
+ ok: true,
1606
+ payload: await exportEngineBundle(request.snapshot)
1607
+ };
1608
+ }
1609
+ if (request.kind === "ai-model-generate") {
1610
+ return {
1611
+ id: request.id,
1612
+ kind: request.kind,
1613
+ ok: true,
1614
+ payload: await generateAiModel(request.prompt)
1615
+ };
1616
+ }
1617
+ return {
1618
+ id: request.id,
1619
+ kind: request.kind,
1620
+ ok: true,
1621
+ payload: await serializeGltfScene(request.snapshot)
1622
+ };
1623
+ } catch (error) {
1624
+ return {
1625
+ id: request.id,
1626
+ kind: request.kind,
1627
+ ok: false,
1628
+ error: error instanceof Error ? error.message : "Unknown worker error."
1629
+ };
1630
+ }
1631
+ }
1632
+ async function generateAiModel(prompt) {
1633
+ const response = await fetch(new URL("/api/ai/models", self.location.origin), {
1634
+ body: JSON.stringify({ prompt }),
1635
+ headers: {
1636
+ "Content-Type": "application/json"
1637
+ },
1638
+ method: "POST"
1639
+ });
1640
+ const payload = await response.text();
1641
+ if (!response.ok) {
1642
+ try {
1643
+ const parsed = JSON.parse(payload);
1644
+ throw new Error(parsed.error ?? "Failed to generate AI model.");
1645
+ } catch {
1646
+ throw new Error(payload || "Failed to generate AI model.");
1647
+ }
1648
+ }
1649
+ return payload;
1650
+ }
1651
+ function serializeWhmap(snapshot) {
1652
+ return JSON.stringify(
1653
+ {
1654
+ format: "whmap",
1655
+ version: 1,
1656
+ scene: snapshot
1657
+ },
1658
+ null,
1659
+ 2
1660
+ );
1661
+ }
1662
+ function parseWhmap(text) {
1663
+ const parsed = JSON.parse(text);
1664
+ if (parsed.format !== "whmap" || !parsed.scene) {
1665
+ throw new Error("Invalid .whmap file.");
1666
+ }
1667
+ return parsed.scene;
1668
+ }
1669
+ async function serializeEngineScene(snapshot) {
1670
+ return serializeRuntimeScene(snapshot);
1671
+ }
1672
+ async function exportEngineBundle(snapshot) {
1673
+ return buildRuntimeBundleFromSnapshot(snapshot);
1674
+ }
1675
+ async function serializeGltfScene(snapshot) {
1676
+ const materialsById = new Map(snapshot.materials.map((material) => [material.id, material]));
1677
+ const assetsById = new Map(snapshot.assets.map((asset) => [asset.id, asset]));
1678
+ const exportedNodes = [];
1679
+ for (const node of snapshot.nodes) {
1680
+ if (isGroupNode(node)) {
1681
+ exportedNodes.push({
1682
+ id: node.id,
1683
+ name: node.name,
1684
+ parentId: node.parentId,
1685
+ rotation: toQuaternion(node.transform.rotation),
1686
+ scale: [node.transform.scale.x, node.transform.scale.y, node.transform.scale.z],
1687
+ translation: [node.transform.position.x, node.transform.position.y, node.transform.position.z]
1688
+ });
1689
+ continue;
1690
+ }
1691
+ if (isBrushNode(node) || isMeshNode(node) || isPrimitiveNode(node)) {
1692
+ const geometry = await buildExportGeometry2(node, materialsById);
1693
+ if (geometry.primitives.length === 0) {
1694
+ continue;
1695
+ }
1696
+ exportedNodes.push({
1697
+ id: node.id,
1698
+ mesh: {
1699
+ name: node.name,
1700
+ primitives: geometry.primitives
1701
+ },
1702
+ meshKey: node.id,
1703
+ name: node.name,
1704
+ parentId: node.parentId,
1705
+ rotation: toQuaternion(node.transform.rotation),
1706
+ scale: [node.transform.scale.x, node.transform.scale.y, node.transform.scale.z],
1707
+ translation: [node.transform.position.x, node.transform.position.y, node.transform.position.z]
1708
+ });
1709
+ continue;
1710
+ }
1711
+ if (isInstancingNode(node)) {
1712
+ const sourceNode = resolveInstancingSourceNode(snapshot.nodes, node);
1713
+ if (!sourceNode || !(isBrushNode(sourceNode) || isMeshNode(sourceNode) || isPrimitiveNode(sourceNode) || isModelNode(sourceNode))) {
1714
+ continue;
1715
+ }
1716
+ const instanceTransform = sanitizeInstanceTransform2(node.transform);
1717
+ if (isModelNode(sourceNode)) {
1718
+ const previewColor = assetsById.get(sourceNode.data.assetId)?.metadata.previewColor;
1719
+ const primitive = createCylinderPrimitive();
1720
+ exportedNodes.push({
1721
+ id: node.id,
1722
+ mesh: {
1723
+ name: sourceNode.name,
1724
+ primitives: [
1725
+ {
1726
+ indices: primitive.indices,
1727
+ material: await resolveExportMaterial({
1728
+ color: typeof previewColor === "string" ? previewColor : "#7f8ea3",
1729
+ id: `material:model:${sourceNode.id}`,
1730
+ metalness: 0.1,
1731
+ name: `${sourceNode.name} Material`,
1732
+ roughness: 0.55
1733
+ }),
1734
+ normals: computePrimitiveNormals(primitive.positions, primitive.indices),
1735
+ positions: primitive.positions,
1736
+ uvs: computeCylinderUvs(primitive.positions)
1737
+ }
1738
+ ]
1739
+ },
1740
+ meshKey: sourceNode.id,
1741
+ name: node.name,
1742
+ parentId: node.parentId,
1743
+ rotation: toQuaternion(instanceTransform.rotation),
1744
+ scale: [instanceTransform.scale.x, instanceTransform.scale.y, instanceTransform.scale.z],
1745
+ translation: [instanceTransform.position.x, instanceTransform.position.y, instanceTransform.position.z]
1746
+ });
1747
+ continue;
1748
+ }
1749
+ const geometry = await buildExportGeometry2(sourceNode, materialsById);
1750
+ if (geometry.primitives.length === 0) {
1751
+ continue;
1752
+ }
1753
+ exportedNodes.push({
1754
+ id: node.id,
1755
+ mesh: {
1756
+ name: sourceNode.name,
1757
+ primitives: geometry.primitives
1758
+ },
1759
+ meshKey: sourceNode.id,
1760
+ name: node.name,
1761
+ parentId: node.parentId,
1762
+ rotation: toQuaternion(instanceTransform.rotation),
1763
+ scale: [instanceTransform.scale.x, instanceTransform.scale.y, instanceTransform.scale.z],
1764
+ translation: [instanceTransform.position.x, instanceTransform.position.y, instanceTransform.position.z]
1765
+ });
1766
+ continue;
1767
+ }
1768
+ if (isModelNode(node)) {
1769
+ const previewColor = assetsById.get(node.data.assetId)?.metadata.previewColor;
1770
+ const primitive = createCylinderPrimitive();
1771
+ exportedNodes.push({
1772
+ id: node.id,
1773
+ mesh: {
1774
+ name: node.name,
1775
+ primitives: [
1776
+ {
1777
+ indices: primitive.indices,
1778
+ material: await resolveExportMaterial({
1779
+ color: typeof previewColor === "string" ? previewColor : "#7f8ea3",
1780
+ id: `material:model:${node.id}`,
1781
+ metalness: 0.1,
1782
+ name: `${node.name} Material`,
1783
+ roughness: 0.55
1784
+ }),
1785
+ normals: computePrimitiveNormals(primitive.positions, primitive.indices),
1786
+ positions: primitive.positions,
1787
+ uvs: computeCylinderUvs(primitive.positions)
1788
+ }
1789
+ ]
1790
+ },
1791
+ meshKey: node.id,
1792
+ name: node.name,
1793
+ parentId: node.parentId,
1794
+ rotation: toQuaternion(node.transform.rotation),
1795
+ scale: [node.transform.scale.x, node.transform.scale.y, node.transform.scale.z],
1796
+ translation: [node.transform.position.x, node.transform.position.y, node.transform.position.z]
1797
+ });
1798
+ }
1799
+ }
1800
+ return buildGltfDocument(exportedNodes);
1801
+ }
1802
+ async function buildGltfDocument(exportedNodes) {
1803
+ const nodes = [];
1804
+ const gltfMeshes = [];
1805
+ const materials = [];
1806
+ const textures = [];
1807
+ const images = [];
1808
+ const samplers = [
1809
+ {
1810
+ magFilter: 9729,
1811
+ minFilter: 9987,
1812
+ wrapS: 10497,
1813
+ wrapT: 10497
1814
+ }
1815
+ ];
1816
+ const accessors = [];
1817
+ const bufferViews = [];
1818
+ const chunks = [];
1819
+ const imageIndexByUri = /* @__PURE__ */ new Map();
1820
+ const textureIndexByUri = /* @__PURE__ */ new Map();
1821
+ const materialIndexById = /* @__PURE__ */ new Map();
1822
+ const meshIndexByKey = /* @__PURE__ */ new Map();
1823
+ const pushBuffer = (bytes, target) => {
1824
+ const padding = (4 - bytes.byteLength % 4) % 4;
1825
+ const padded = new Uint8Array(bytes.byteLength + padding);
1826
+ padded.set(bytes);
1827
+ const byteOffset = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
1828
+ chunks.push(padded);
1829
+ bufferViews.push({
1830
+ buffer: 0,
1831
+ byteLength: bytes.byteLength,
1832
+ byteOffset,
1833
+ ...target ? { target } : {}
1834
+ });
1835
+ return bufferViews.length - 1;
1836
+ };
1837
+ const nodeIndexById = /* @__PURE__ */ new Map();
1838
+ for (const exportedNode of exportedNodes) {
1839
+ let meshIndex;
1840
+ if (exportedNode.mesh) {
1841
+ const meshKey = exportedNode.meshKey ?? exportedNode.id;
1842
+ const cachedMeshIndex = meshIndexByKey.get(meshKey);
1843
+ if (cachedMeshIndex !== void 0) {
1844
+ meshIndex = cachedMeshIndex;
1845
+ } else {
1846
+ const gltfPrimitives = [];
1847
+ for (const primitive of exportedNode.mesh.primitives) {
1848
+ const positions = new Float32Array(primitive.positions);
1849
+ const normals = new Float32Array(primitive.normals);
1850
+ const uvs = new Float32Array(primitive.uvs);
1851
+ const indices = new Uint32Array(primitive.indices);
1852
+ const positionView = pushBuffer(new Uint8Array(positions.buffer.slice(0)), 34962);
1853
+ const normalView = pushBuffer(new Uint8Array(normals.buffer.slice(0)), 34962);
1854
+ const uvView = pushBuffer(new Uint8Array(uvs.buffer.slice(0)), 34962);
1855
+ const indexView = pushBuffer(new Uint8Array(indices.buffer.slice(0)), 34963);
1856
+ const bounds = computePositionBounds(primitive.positions);
1857
+ accessors.push({
1858
+ bufferView: positionView,
1859
+ componentType: 5126,
1860
+ count: positions.length / 3,
1861
+ max: bounds.max,
1862
+ min: bounds.min,
1863
+ type: "VEC3"
1864
+ });
1865
+ const positionAccessor = accessors.length - 1;
1866
+ accessors.push({
1867
+ bufferView: normalView,
1868
+ componentType: 5126,
1869
+ count: normals.length / 3,
1870
+ type: "VEC3"
1871
+ });
1872
+ const normalAccessor = accessors.length - 1;
1873
+ accessors.push({
1874
+ bufferView: uvView,
1875
+ componentType: 5126,
1876
+ count: uvs.length / 2,
1877
+ type: "VEC2"
1878
+ });
1879
+ const uvAccessor = accessors.length - 1;
1880
+ accessors.push({
1881
+ bufferView: indexView,
1882
+ componentType: 5125,
1883
+ count: indices.length,
1884
+ type: "SCALAR"
1885
+ });
1886
+ const indexAccessor = accessors.length - 1;
1887
+ const materialIndex = await ensureGltfMaterial(
1888
+ primitive.material,
1889
+ materials,
1890
+ textures,
1891
+ images,
1892
+ imageIndexByUri,
1893
+ textureIndexByUri,
1894
+ materialIndexById
1895
+ );
1896
+ gltfPrimitives.push({
1897
+ attributes: {
1898
+ NORMAL: normalAccessor,
1899
+ POSITION: positionAccessor,
1900
+ TEXCOORD_0: uvAccessor
1901
+ },
1902
+ indices: indexAccessor,
1903
+ material: materialIndex
1904
+ });
1905
+ }
1906
+ gltfMeshes.push({
1907
+ name: exportedNode.mesh.name,
1908
+ primitives: gltfPrimitives
1909
+ });
1910
+ meshIndex = gltfMeshes.length - 1;
1911
+ meshIndexByKey.set(meshKey, meshIndex);
1912
+ }
1913
+ }
1914
+ nodes.push({
1915
+ ...meshIndex !== void 0 ? { mesh: meshIndex } : {},
1916
+ name: exportedNode.name,
1917
+ ...exportedNode.rotation ? { rotation: exportedNode.rotation } : {},
1918
+ scale: exportedNode.scale,
1919
+ translation: exportedNode.translation
1920
+ });
1921
+ nodeIndexById.set(exportedNode.id, nodes.length - 1);
1922
+ }
1923
+ const rootNodeIndices = [];
1924
+ exportedNodes.forEach((exportedNode, index) => {
1925
+ const parentIndex = exportedNode.parentId ? nodeIndexById.get(exportedNode.parentId) : void 0;
1926
+ if (parentIndex === void 0) {
1927
+ rootNodeIndices.push(index);
1928
+ return;
1929
+ }
1930
+ const parent = nodes[parentIndex];
1931
+ parent.children = [...parent.children ?? [], index];
1932
+ });
1933
+ const totalByteLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
1934
+ const merged = new Uint8Array(totalByteLength);
1935
+ let cursor = 0;
1936
+ chunks.forEach((chunk) => {
1937
+ merged.set(chunk, cursor);
1938
+ cursor += chunk.byteLength;
1939
+ });
1940
+ const gltf = {
1941
+ accessors,
1942
+ asset: {
1943
+ generator: "web-hammer",
1944
+ version: "2.0"
1945
+ },
1946
+ bufferViews,
1947
+ buffers: [
1948
+ {
1949
+ byteLength: merged.byteLength,
1950
+ uri: `data:application/octet-stream;base64,${toBase642(merged)}`
1951
+ }
1952
+ ],
1953
+ images,
1954
+ materials,
1955
+ meshes: gltfMeshes,
1956
+ nodes,
1957
+ samplers,
1958
+ scene: 0,
1959
+ scenes: [
1960
+ {
1961
+ nodes: rootNodeIndices
1962
+ }
1963
+ ],
1964
+ textures
1965
+ };
1966
+ return JSON.stringify(gltf, null, 2);
1967
+ }
1968
+ async function buildExportGeometry2(node, materialsById) {
1969
+ const fallbackMaterial = await resolveExportMaterial({
1970
+ color: node.kind === "brush" ? "#f69036" : node.kind === "primitive" && node.data.role === "prop" ? "#7f8ea3" : "#6ed5c0",
1971
+ id: `material:fallback:${node.id}`,
1972
+ metalness: node.kind === "brush" ? 0 : node.kind === "primitive" && node.data.role === "prop" ? 0.12 : 0.05,
1973
+ name: `${node.name} Default`,
1974
+ roughness: node.kind === "brush" ? 0.95 : node.kind === "primitive" && node.data.role === "prop" ? 0.64 : 0.82
1975
+ });
1976
+ const primitiveByMaterial = /* @__PURE__ */ new Map();
1977
+ const appendFace = async (params) => {
1978
+ const material = params.faceMaterialId ? await resolveExportMaterial(materialsById.get(params.faceMaterialId)) : fallbackMaterial;
1979
+ const primitive = primitiveByMaterial.get(material.id) ?? {
1980
+ indices: [],
1981
+ material,
1982
+ normals: [],
1983
+ positions: [],
1984
+ uvs: []
1985
+ };
1986
+ const vertexOffset = primitive.positions.length / 3;
1987
+ const uvs = params.uvs && params.uvs.length === params.vertices.length ? params.uvs.flatMap((uv) => [uv.x, uv.y]) : projectPlanarUvs2(params.vertices, params.normal, params.uvScale, params.uvOffset);
1988
+ params.vertices.forEach((vertex) => {
1989
+ primitive.positions.push(vertex.x, vertex.y, vertex.z);
1990
+ primitive.normals.push(params.normal.x, params.normal.y, params.normal.z);
1991
+ });
1992
+ primitive.uvs.push(...uvs);
1993
+ params.triangleIndices.forEach((index) => {
1994
+ primitive.indices.push(vertexOffset + index);
1995
+ });
1996
+ primitiveByMaterial.set(material.id, primitive);
1997
+ };
1998
+ if (isBrushNode(node)) {
1999
+ const rebuilt = reconstructBrushFaces(node.data);
2000
+ if (!rebuilt.valid) {
2001
+ return { primitives: [] };
2002
+ }
2003
+ for (const face of rebuilt.faces) {
2004
+ await appendFace({
2005
+ faceMaterialId: face.materialId,
2006
+ normal: face.normal,
2007
+ triangleIndices: face.triangleIndices,
2008
+ uvOffset: face.uvOffset,
2009
+ uvScale: face.uvScale,
2010
+ vertices: face.vertices.map((vertex) => vertex.position)
2011
+ });
2012
+ }
2013
+ }
2014
+ if (isMeshNode(node)) {
2015
+ for (const face of node.data.faces) {
2016
+ const triangulated = triangulateMeshFace(node.data, face.id);
2017
+ if (!triangulated) {
2018
+ continue;
2019
+ }
2020
+ await appendFace({
2021
+ faceMaterialId: face.materialId,
2022
+ normal: triangulated.normal,
2023
+ triangleIndices: triangulated.indices,
2024
+ uvOffset: face.uvOffset,
2025
+ uvScale: face.uvScale,
2026
+ uvs: face.uvs,
2027
+ vertices: getFaceVertices(node.data, face.id).map((vertex) => vertex.position)
2028
+ });
2029
+ }
2030
+ }
2031
+ if (isPrimitiveNode(node)) {
2032
+ const material = node.data.materialId ? await resolveExportMaterial(materialsById.get(node.data.materialId)) : fallbackMaterial;
2033
+ const primitive = buildPrimitiveGeometry2(node.data.shape, node.data.size, node.data.radialSegments ?? 24);
2034
+ if (primitive) {
2035
+ primitiveByMaterial.set(material.id, {
2036
+ indices: primitive.indices,
2037
+ material,
2038
+ normals: primitive.normals,
2039
+ positions: primitive.positions,
2040
+ uvs: primitive.uvs
2041
+ });
2042
+ }
2043
+ }
2044
+ return {
2045
+ primitives: Array.from(primitiveByMaterial.values())
2046
+ };
2047
+ }
2048
+ function sanitizeInstanceTransform2(transform) {
2049
+ return {
2050
+ position: structuredClone(transform.position),
2051
+ rotation: structuredClone(transform.rotation),
2052
+ scale: structuredClone(transform.scale)
2053
+ };
2054
+ }
2055
+ function buildPrimitiveGeometry2(shape, size, radialSegments) {
2056
+ const geometry = shape === "cube" ? new BoxGeometry2(Math.abs(size.x), Math.abs(size.y), Math.abs(size.z)) : shape === "sphere" ? new SphereGeometry2(Math.max(Math.abs(size.x), Math.abs(size.z)) * 0.5, radialSegments, Math.max(8, Math.floor(radialSegments * 0.75))) : shape === "cylinder" ? new CylinderGeometry2(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 ConeGeometry2(Math.max(Math.abs(size.x), Math.abs(size.z)) * 0.5, Math.abs(size.y), radialSegments);
2057
+ const positionAttribute = geometry.getAttribute("position");
2058
+ const normalAttribute = geometry.getAttribute("normal");
2059
+ const uvAttribute = geometry.getAttribute("uv");
2060
+ const index = geometry.getIndex();
2061
+ const primitive = {
2062
+ indices: index ? Array.from(index.array) : Array.from({ length: positionAttribute.count }, (_, value) => value),
2063
+ normals: Array.from(normalAttribute.array),
2064
+ positions: Array.from(positionAttribute.array),
2065
+ uvs: uvAttribute ? Array.from(uvAttribute.array) : []
2066
+ };
2067
+ geometry.dispose();
2068
+ return primitive;
2069
+ }
2070
+ async function resolveExportMaterial(material) {
2071
+ const resolved = material ?? {
2072
+ color: "#ffffff",
2073
+ id: "material:fallback:default",
2074
+ metalness: 0.05,
2075
+ name: "Default Material",
2076
+ roughness: 0.8
2077
+ };
2078
+ return {
2079
+ baseColorTexture: await resolveEmbeddedTextureUri2(resolved.colorTexture ?? resolveGeneratedBlockoutTexture2(resolved)),
2080
+ color: resolved.color,
2081
+ id: resolved.id,
2082
+ metallicFactor: resolved.metalness ?? 0,
2083
+ metallicRoughnessTexture: await createMetallicRoughnessTextureDataUri2(
2084
+ resolved.metalnessTexture,
2085
+ resolved.roughnessTexture,
2086
+ resolved.metalness ?? 0,
2087
+ resolved.roughness ?? 0.8
2088
+ ),
2089
+ name: resolved.name,
2090
+ normalTexture: await resolveEmbeddedTextureUri2(resolved.normalTexture),
2091
+ roughnessFactor: resolved.roughness ?? 0.8,
2092
+ side: resolved.side
2093
+ };
2094
+ }
2095
+ function resolveGeneratedBlockoutTexture2(material) {
2096
+ return material.category === "blockout" ? createBlockoutTextureDataUri(material.color, material.edgeColor ?? "#2f3540", material.edgeThickness ?? 0.035) : void 0;
2097
+ }
2098
+ async function ensureGltfMaterial(material, materials, textures, images, imageIndexByUri, textureIndexByUri, materialIndexById) {
2099
+ const existing = materialIndexById.get(material.id);
2100
+ if (existing !== void 0) {
2101
+ return existing;
2102
+ }
2103
+ const baseColorTextureIndex = material.baseColorTexture ? ensureGltfTexture(material.baseColorTexture, textures, images, imageIndexByUri, textureIndexByUri) : void 0;
2104
+ const normalTextureIndex = material.normalTexture ? ensureGltfTexture(material.normalTexture, textures, images, imageIndexByUri, textureIndexByUri) : void 0;
2105
+ const metallicRoughnessTextureIndex = material.metallicRoughnessTexture ? ensureGltfTexture(material.metallicRoughnessTexture, textures, images, imageIndexByUri, textureIndexByUri) : void 0;
2106
+ materials.push({
2107
+ name: material.name,
2108
+ normalTexture: normalTextureIndex !== void 0 ? { index: normalTextureIndex } : void 0,
2109
+ pbrMetallicRoughness: {
2110
+ ...baseColorTextureIndex !== void 0 ? { baseColorTexture: { index: baseColorTextureIndex } } : {},
2111
+ ...metallicRoughnessTextureIndex !== void 0 ? { metallicRoughnessTexture: { index: metallicRoughnessTextureIndex } } : {},
2112
+ baseColorFactor: hexToRgba(material.color),
2113
+ metallicFactor: material.metallicFactor,
2114
+ roughnessFactor: material.roughnessFactor
2115
+ }
2116
+ });
2117
+ const index = materials.length - 1;
2118
+ materialIndexById.set(material.id, index);
2119
+ return index;
2120
+ }
2121
+ function ensureGltfTexture(uri, textures, images, imageIndexByUri, textureIndexByUri) {
2122
+ const existingTexture = textureIndexByUri.get(uri);
2123
+ if (existingTexture !== void 0) {
2124
+ return existingTexture;
2125
+ }
2126
+ const imageIndex = imageIndexByUri.get(uri) ?? images.length;
2127
+ if (!imageIndexByUri.has(uri)) {
2128
+ images.push({ uri });
2129
+ imageIndexByUri.set(uri, imageIndex);
2130
+ }
2131
+ textures.push({ sampler: 0, source: imageIndex });
2132
+ const textureIndex = textures.length - 1;
2133
+ textureIndexByUri.set(uri, textureIndex);
2134
+ return textureIndex;
2135
+ }
2136
+ function projectPlanarUvs2(vertices, normal, uvScale, uvOffset) {
2137
+ const basis = createFacePlaneBasis2(normal);
2138
+ const origin = vertices[0] ?? vec3(0, 0, 0);
2139
+ const scaleX = Math.abs(uvScale?.x ?? 1) <= 1e-4 ? 1 : uvScale?.x ?? 1;
2140
+ const scaleY = Math.abs(uvScale?.y ?? 1) <= 1e-4 ? 1 : uvScale?.y ?? 1;
2141
+ const offsetX = uvOffset?.x ?? 0;
2142
+ const offsetY = uvOffset?.y ?? 0;
2143
+ return vertices.flatMap((vertex) => {
2144
+ const offset = subVec3(vertex, origin);
2145
+ return [dotVec3(offset, basis.u) * scaleX + offsetX, dotVec3(offset, basis.v) * scaleY + offsetY];
2146
+ });
2147
+ }
2148
+ function createFacePlaneBasis2(normal) {
2149
+ const normalizedNormal = normalizeVec3(normal);
2150
+ const reference = Math.abs(normalizedNormal.y) < 0.99 ? vec3(0, 1, 0) : vec3(1, 0, 0);
2151
+ const u = normalizeVec3(crossVec3(reference, normalizedNormal));
2152
+ const v = normalizeVec3(crossVec3(normalizedNormal, u));
2153
+ return { u, v };
2154
+ }
2155
+ async function resolveEmbeddedTextureUri2(source) {
2156
+ if (!source) {
2157
+ return void 0;
2158
+ }
2159
+ if (source.startsWith("data:")) {
2160
+ return source;
2161
+ }
2162
+ const response = await fetch(source);
2163
+ const blob = await response.blob();
2164
+ const buffer = await blob.arrayBuffer();
2165
+ return `data:${blob.type || "application/octet-stream"};base64,${toBase642(new Uint8Array(buffer))}`;
2166
+ }
2167
+ async function createMetallicRoughnessTextureDataUri2(metalnessSource, roughnessSource, metalnessFactor, roughnessFactor) {
2168
+ if (!metalnessSource && !roughnessSource) {
2169
+ return void 0;
2170
+ }
2171
+ if (typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {
2172
+ return void 0;
2173
+ }
2174
+ const [metalness, roughness] = await Promise.all([
2175
+ loadImagePixels2(metalnessSource),
2176
+ loadImagePixels2(roughnessSource)
2177
+ ]);
2178
+ const width = Math.max(metalness?.width ?? 1, roughness?.width ?? 1);
2179
+ const height = Math.max(metalness?.height ?? 1, roughness?.height ?? 1);
2180
+ const canvas = new OffscreenCanvas(width, height);
2181
+ const context = canvas.getContext("2d");
2182
+ if (!context) {
2183
+ return void 0;
2184
+ }
2185
+ const imageData = context.createImageData(width, height);
2186
+ const metalDefault = Math.round(clamp012(metalnessFactor) * 255);
2187
+ const roughDefault = Math.round(clamp012(roughnessFactor) * 255);
2188
+ for (let index = 0; index < imageData.data.length; index += 4) {
2189
+ imageData.data[index] = 0;
2190
+ imageData.data[index + 1] = roughness?.pixels[index] ?? roughDefault;
2191
+ imageData.data[index + 2] = metalness?.pixels[index] ?? metalDefault;
2192
+ imageData.data[index + 3] = 255;
2193
+ }
2194
+ context.putImageData(imageData, 0, 0);
2195
+ const blob = await canvas.convertToBlob({ type: "image/png" });
2196
+ const buffer = await blob.arrayBuffer();
2197
+ return `data:image/png;base64,${toBase642(new Uint8Array(buffer))}`;
2198
+ }
2199
+ async function loadImagePixels2(source) {
2200
+ if (!source || typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {
2201
+ return void 0;
2202
+ }
2203
+ const response = await fetch(source);
2204
+ const blob = await response.blob();
2205
+ const bitmap = await createImageBitmap(blob);
2206
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
2207
+ const context = canvas.getContext("2d", { willReadFrequently: true });
2208
+ if (!context) {
2209
+ bitmap.close();
2210
+ return void 0;
2211
+ }
2212
+ context.drawImage(bitmap, 0, 0);
2213
+ bitmap.close();
2214
+ const imageData = context.getImageData(0, 0, bitmap.width, bitmap.height);
2215
+ return {
2216
+ height: imageData.height,
2217
+ pixels: imageData.data,
2218
+ width: imageData.width
2219
+ };
2220
+ }
2221
+ function computePrimitiveNormals(positions, indices) {
2222
+ const normals = new Array(positions.length).fill(0);
2223
+ for (let index = 0; index < indices.length; index += 3) {
2224
+ const a = indices[index] * 3;
2225
+ const b = indices[index + 1] * 3;
2226
+ const c = indices[index + 2] * 3;
2227
+ const normal = normalizeVec3(
2228
+ crossVec3(
2229
+ vec3(positions[b] - positions[a], positions[b + 1] - positions[a + 1], positions[b + 2] - positions[a + 2]),
2230
+ vec3(positions[c] - positions[a], positions[c + 1] - positions[a + 1], positions[c + 2] - positions[a + 2])
2231
+ )
2232
+ );
2233
+ [a, b, c].forEach((offset) => {
2234
+ normals[offset] = normal.x;
2235
+ normals[offset + 1] = normal.y;
2236
+ normals[offset + 2] = normal.z;
2237
+ });
2238
+ }
2239
+ return normals;
2240
+ }
2241
+ function computeCylinderUvs(positions) {
2242
+ const uvs = [];
2243
+ for (let index = 0; index < positions.length; index += 3) {
2244
+ const x = positions[index];
2245
+ const y = positions[index + 1];
2246
+ const z = positions[index + 2];
2247
+ const u = (Math.atan2(z, x) / (Math.PI * 2) + 1) % 1;
2248
+ const v = y > 0 ? 1 : 0;
2249
+ uvs.push(u, v);
2250
+ }
2251
+ return uvs;
2252
+ }
2253
+ function clamp012(value) {
2254
+ return Math.max(0, Math.min(1, value));
2255
+ }
2256
+ function toQuaternion(rotation) {
2257
+ const quaternion = new Quaternion2().setFromEuler(new Euler2(rotation.x, rotation.y, rotation.z, "XYZ"));
2258
+ return [quaternion.x, quaternion.y, quaternion.z, quaternion.w];
2259
+ }
2260
+ function createCylinderPrimitive() {
2261
+ const radius = 0.65;
2262
+ const halfHeight = 1.1;
2263
+ const segments = 12;
2264
+ const positions = [];
2265
+ const indices = [];
2266
+ for (let index = 0; index < segments; index += 1) {
2267
+ const angle = index / segments * Math.PI * 2;
2268
+ const x = Math.cos(angle) * radius;
2269
+ const z = Math.sin(angle) * radius;
2270
+ positions.push(x, -halfHeight, z, x, halfHeight, z);
2271
+ }
2272
+ for (let index = 0; index < segments; index += 1) {
2273
+ const next = (index + 1) % segments;
2274
+ const bottom = index * 2;
2275
+ const top = bottom + 1;
2276
+ const nextBottom = next * 2;
2277
+ const nextTop = nextBottom + 1;
2278
+ indices.push(bottom, nextBottom, top, top, nextBottom, nextTop);
2279
+ }
2280
+ return { indices, positions };
2281
+ }
2282
+ function computePositionBounds(positions) {
2283
+ const min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
2284
+ const max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
2285
+ for (let index = 0; index < positions.length; index += 3) {
2286
+ min[0] = Math.min(min[0], positions[index]);
2287
+ min[1] = Math.min(min[1], positions[index + 1]);
2288
+ min[2] = Math.min(min[2], positions[index + 2]);
2289
+ max[0] = Math.max(max[0], positions[index]);
2290
+ max[1] = Math.max(max[1], positions[index + 1]);
2291
+ max[2] = Math.max(max[2], positions[index + 2]);
2292
+ }
2293
+ return { max, min };
2294
+ }
2295
+ function toBase642(bytes) {
2296
+ let binary = "";
2297
+ bytes.forEach((byte) => {
2298
+ binary += String.fromCharCode(byte);
2299
+ });
2300
+ return btoa(binary);
2301
+ }
2302
+ function hexToRgba(hex) {
2303
+ const normalized = hex.replace("#", "");
2304
+ const parsed = Number.parseInt(normalized, 16);
2305
+ return [(parsed >> 16 & 255) / 255, (parsed >> 8 & 255) / 255, (parsed & 255) / 255, 1];
2306
+ }
2307
+
2308
+ // src/tasks.ts
2309
+ var workerIds = ["geometryWorker", "meshWorker", "navWorker", "exportWorker"];
2310
+ export {
2311
+ createWorkerTaskManager,
2312
+ executeWorkerRequest,
2313
+ exportEngineBundle,
2314
+ parseWhmap,
2315
+ serializeEngineScene,
2316
+ serializeGltfScene,
2317
+ serializeWhmap,
2318
+ workerIds
2319
+ };
2320
+ //# sourceMappingURL=index.js.map