@ggez/workers 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 +76 -1552
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -54,1532 +54,56 @@ function createWorkerTaskManager() {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
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
57
|
// src/export-tasks.ts
|
|
1555
|
-
import {
|
|
58
|
+
import { getFaceVertices, reconstructBrushFaces, triangulateMeshFace } from "@ggez/geometry-kernel";
|
|
59
|
+
import {
|
|
60
|
+
createBlockoutTextureDataUri,
|
|
61
|
+
crossVec3,
|
|
62
|
+
dotVec3,
|
|
63
|
+
isBrushNode,
|
|
64
|
+
isGroupNode,
|
|
65
|
+
isInstancingNode,
|
|
66
|
+
isMeshNode,
|
|
67
|
+
isModelNode,
|
|
68
|
+
isPrimitiveNode,
|
|
69
|
+
normalizeVec3,
|
|
70
|
+
resolveInstancingSourceNode,
|
|
71
|
+
subVec3,
|
|
72
|
+
vec3
|
|
73
|
+
} from "@ggez/shared";
|
|
1556
74
|
import {
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
75
|
+
buildRuntimeBundleFromSnapshot,
|
|
76
|
+
buildRuntimeSceneFromSnapshot,
|
|
77
|
+
serializeRuntimeScene
|
|
78
|
+
} from "@ggez/runtime-build";
|
|
79
|
+
import { MeshBVH } from "three-mesh-bvh";
|
|
80
|
+
import {
|
|
81
|
+
Box3,
|
|
82
|
+
BoxGeometry,
|
|
83
|
+
BufferGeometry,
|
|
84
|
+
ConeGeometry,
|
|
85
|
+
CylinderGeometry,
|
|
86
|
+
Euler,
|
|
87
|
+
Float32BufferAttribute,
|
|
88
|
+
Group,
|
|
89
|
+
Mesh,
|
|
90
|
+
MeshStandardMaterial,
|
|
91
|
+
Quaternion,
|
|
92
|
+
RepeatWrapping,
|
|
93
|
+
Scene,
|
|
94
|
+
SphereGeometry,
|
|
95
|
+
SRGBColorSpace,
|
|
96
|
+
TextureLoader,
|
|
97
|
+
Vector3
|
|
1574
98
|
} from "three";
|
|
1575
|
-
import { GLTFExporter
|
|
1576
|
-
import { GLTFLoader
|
|
1577
|
-
import { MTLLoader
|
|
1578
|
-
import { OBJLoader
|
|
1579
|
-
var
|
|
1580
|
-
var
|
|
1581
|
-
var
|
|
1582
|
-
var
|
|
99
|
+
import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
|
|
100
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
101
|
+
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader.js";
|
|
102
|
+
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
|
|
103
|
+
var gltfLoader = new GLTFLoader();
|
|
104
|
+
var gltfExporter = new GLTFExporter();
|
|
105
|
+
var mtlLoader = new MTLLoader();
|
|
106
|
+
var modelTextureLoader = new TextureLoader();
|
|
1583
107
|
async function executeWorkerRequest(request) {
|
|
1584
108
|
try {
|
|
1585
109
|
if (request.kind === "whmap-save") {
|
|
@@ -1689,7 +213,7 @@ async function serializeGltfScene(snapshot) {
|
|
|
1689
213
|
continue;
|
|
1690
214
|
}
|
|
1691
215
|
if (isBrushNode(node) || isMeshNode(node) || isPrimitiveNode(node)) {
|
|
1692
|
-
const geometry = await
|
|
216
|
+
const geometry = await buildExportGeometry(node, materialsById);
|
|
1693
217
|
if (geometry.primitives.length === 0) {
|
|
1694
218
|
continue;
|
|
1695
219
|
}
|
|
@@ -1713,7 +237,7 @@ async function serializeGltfScene(snapshot) {
|
|
|
1713
237
|
if (!sourceNode || !(isBrushNode(sourceNode) || isMeshNode(sourceNode) || isPrimitiveNode(sourceNode) || isModelNode(sourceNode))) {
|
|
1714
238
|
continue;
|
|
1715
239
|
}
|
|
1716
|
-
const instanceTransform =
|
|
240
|
+
const instanceTransform = sanitizeInstanceTransform(node.transform);
|
|
1717
241
|
if (isModelNode(sourceNode)) {
|
|
1718
242
|
const previewColor = assetsById.get(sourceNode.data.assetId)?.metadata.previewColor;
|
|
1719
243
|
const primitive = createCylinderPrimitive();
|
|
@@ -1746,7 +270,7 @@ async function serializeGltfScene(snapshot) {
|
|
|
1746
270
|
});
|
|
1747
271
|
continue;
|
|
1748
272
|
}
|
|
1749
|
-
const geometry = await
|
|
273
|
+
const geometry = await buildExportGeometry(sourceNode, materialsById);
|
|
1750
274
|
if (geometry.primitives.length === 0) {
|
|
1751
275
|
continue;
|
|
1752
276
|
}
|
|
@@ -1947,7 +471,7 @@ async function buildGltfDocument(exportedNodes) {
|
|
|
1947
471
|
buffers: [
|
|
1948
472
|
{
|
|
1949
473
|
byteLength: merged.byteLength,
|
|
1950
|
-
uri: `data:application/octet-stream;base64,${
|
|
474
|
+
uri: `data:application/octet-stream;base64,${toBase64(merged)}`
|
|
1951
475
|
}
|
|
1952
476
|
],
|
|
1953
477
|
images,
|
|
@@ -1965,7 +489,7 @@ async function buildGltfDocument(exportedNodes) {
|
|
|
1965
489
|
};
|
|
1966
490
|
return JSON.stringify(gltf, null, 2);
|
|
1967
491
|
}
|
|
1968
|
-
async function
|
|
492
|
+
async function buildExportGeometry(node, materialsById) {
|
|
1969
493
|
const fallbackMaterial = await resolveExportMaterial({
|
|
1970
494
|
color: node.kind === "brush" ? "#f69036" : node.kind === "primitive" && node.data.role === "prop" ? "#7f8ea3" : "#6ed5c0",
|
|
1971
495
|
id: `material:fallback:${node.id}`,
|
|
@@ -1984,7 +508,7 @@ async function buildExportGeometry2(node, materialsById) {
|
|
|
1984
508
|
uvs: []
|
|
1985
509
|
};
|
|
1986
510
|
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]) :
|
|
511
|
+
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);
|
|
1988
512
|
params.vertices.forEach((vertex) => {
|
|
1989
513
|
primitive.positions.push(vertex.x, vertex.y, vertex.z);
|
|
1990
514
|
primitive.normals.push(params.normal.x, params.normal.y, params.normal.z);
|
|
@@ -2030,7 +554,7 @@ async function buildExportGeometry2(node, materialsById) {
|
|
|
2030
554
|
}
|
|
2031
555
|
if (isPrimitiveNode(node)) {
|
|
2032
556
|
const material = node.data.materialId ? await resolveExportMaterial(materialsById.get(node.data.materialId)) : fallbackMaterial;
|
|
2033
|
-
const primitive =
|
|
557
|
+
const primitive = buildPrimitiveGeometry(node.data.shape, node.data.size, node.data.radialSegments ?? 24);
|
|
2034
558
|
if (primitive) {
|
|
2035
559
|
primitiveByMaterial.set(material.id, {
|
|
2036
560
|
indices: primitive.indices,
|
|
@@ -2045,15 +569,15 @@ async function buildExportGeometry2(node, materialsById) {
|
|
|
2045
569
|
primitives: Array.from(primitiveByMaterial.values())
|
|
2046
570
|
};
|
|
2047
571
|
}
|
|
2048
|
-
function
|
|
572
|
+
function sanitizeInstanceTransform(transform) {
|
|
2049
573
|
return {
|
|
2050
574
|
position: structuredClone(transform.position),
|
|
2051
575
|
rotation: structuredClone(transform.rotation),
|
|
2052
576
|
scale: structuredClone(transform.scale)
|
|
2053
577
|
};
|
|
2054
578
|
}
|
|
2055
|
-
function
|
|
2056
|
-
const geometry = shape === "cube" ? new
|
|
579
|
+
function buildPrimitiveGeometry(shape, size, radialSegments) {
|
|
580
|
+
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);
|
|
2057
581
|
const positionAttribute = geometry.getAttribute("position");
|
|
2058
582
|
const normalAttribute = geometry.getAttribute("normal");
|
|
2059
583
|
const uvAttribute = geometry.getAttribute("uv");
|
|
@@ -2076,23 +600,23 @@ async function resolveExportMaterial(material) {
|
|
|
2076
600
|
roughness: 0.8
|
|
2077
601
|
};
|
|
2078
602
|
return {
|
|
2079
|
-
baseColorTexture: await
|
|
603
|
+
baseColorTexture: await resolveEmbeddedTextureUri(resolved.colorTexture ?? resolveGeneratedBlockoutTexture(resolved)),
|
|
2080
604
|
color: resolved.color,
|
|
2081
605
|
id: resolved.id,
|
|
2082
606
|
metallicFactor: resolved.metalness ?? 0,
|
|
2083
|
-
metallicRoughnessTexture: await
|
|
607
|
+
metallicRoughnessTexture: await createMetallicRoughnessTextureDataUri(
|
|
2084
608
|
resolved.metalnessTexture,
|
|
2085
609
|
resolved.roughnessTexture,
|
|
2086
610
|
resolved.metalness ?? 0,
|
|
2087
611
|
resolved.roughness ?? 0.8
|
|
2088
612
|
),
|
|
2089
613
|
name: resolved.name,
|
|
2090
|
-
normalTexture: await
|
|
614
|
+
normalTexture: await resolveEmbeddedTextureUri(resolved.normalTexture),
|
|
2091
615
|
roughnessFactor: resolved.roughness ?? 0.8,
|
|
2092
616
|
side: resolved.side
|
|
2093
617
|
};
|
|
2094
618
|
}
|
|
2095
|
-
function
|
|
619
|
+
function resolveGeneratedBlockoutTexture(material) {
|
|
2096
620
|
return material.category === "blockout" ? createBlockoutTextureDataUri(material.color, material.edgeColor ?? "#2f3540", material.edgeThickness ?? 0.035) : void 0;
|
|
2097
621
|
}
|
|
2098
622
|
async function ensureGltfMaterial(material, materials, textures, images, imageIndexByUri, textureIndexByUri, materialIndexById) {
|
|
@@ -2133,8 +657,8 @@ function ensureGltfTexture(uri, textures, images, imageIndexByUri, textureIndexB
|
|
|
2133
657
|
textureIndexByUri.set(uri, textureIndex);
|
|
2134
658
|
return textureIndex;
|
|
2135
659
|
}
|
|
2136
|
-
function
|
|
2137
|
-
const basis =
|
|
660
|
+
function projectPlanarUvs(vertices, normal, uvScale, uvOffset) {
|
|
661
|
+
const basis = createFacePlaneBasis(normal);
|
|
2138
662
|
const origin = vertices[0] ?? vec3(0, 0, 0);
|
|
2139
663
|
const scaleX = Math.abs(uvScale?.x ?? 1) <= 1e-4 ? 1 : uvScale?.x ?? 1;
|
|
2140
664
|
const scaleY = Math.abs(uvScale?.y ?? 1) <= 1e-4 ? 1 : uvScale?.y ?? 1;
|
|
@@ -2145,14 +669,14 @@ function projectPlanarUvs2(vertices, normal, uvScale, uvOffset) {
|
|
|
2145
669
|
return [dotVec3(offset, basis.u) * scaleX + offsetX, dotVec3(offset, basis.v) * scaleY + offsetY];
|
|
2146
670
|
});
|
|
2147
671
|
}
|
|
2148
|
-
function
|
|
672
|
+
function createFacePlaneBasis(normal) {
|
|
2149
673
|
const normalizedNormal = normalizeVec3(normal);
|
|
2150
674
|
const reference = Math.abs(normalizedNormal.y) < 0.99 ? vec3(0, 1, 0) : vec3(1, 0, 0);
|
|
2151
675
|
const u = normalizeVec3(crossVec3(reference, normalizedNormal));
|
|
2152
676
|
const v = normalizeVec3(crossVec3(normalizedNormal, u));
|
|
2153
677
|
return { u, v };
|
|
2154
678
|
}
|
|
2155
|
-
async function
|
|
679
|
+
async function resolveEmbeddedTextureUri(source) {
|
|
2156
680
|
if (!source) {
|
|
2157
681
|
return void 0;
|
|
2158
682
|
}
|
|
@@ -2162,9 +686,9 @@ async function resolveEmbeddedTextureUri2(source) {
|
|
|
2162
686
|
const response = await fetch(source);
|
|
2163
687
|
const blob = await response.blob();
|
|
2164
688
|
const buffer = await blob.arrayBuffer();
|
|
2165
|
-
return `data:${blob.type || "application/octet-stream"};base64,${
|
|
689
|
+
return `data:${blob.type || "application/octet-stream"};base64,${toBase64(new Uint8Array(buffer))}`;
|
|
2166
690
|
}
|
|
2167
|
-
async function
|
|
691
|
+
async function createMetallicRoughnessTextureDataUri(metalnessSource, roughnessSource, metalnessFactor, roughnessFactor) {
|
|
2168
692
|
if (!metalnessSource && !roughnessSource) {
|
|
2169
693
|
return void 0;
|
|
2170
694
|
}
|
|
@@ -2172,8 +696,8 @@ async function createMetallicRoughnessTextureDataUri2(metalnessSource, roughness
|
|
|
2172
696
|
return void 0;
|
|
2173
697
|
}
|
|
2174
698
|
const [metalness, roughness] = await Promise.all([
|
|
2175
|
-
|
|
2176
|
-
|
|
699
|
+
loadImagePixels(metalnessSource),
|
|
700
|
+
loadImagePixels(roughnessSource)
|
|
2177
701
|
]);
|
|
2178
702
|
const width = Math.max(metalness?.width ?? 1, roughness?.width ?? 1);
|
|
2179
703
|
const height = Math.max(metalness?.height ?? 1, roughness?.height ?? 1);
|
|
@@ -2183,8 +707,8 @@ async function createMetallicRoughnessTextureDataUri2(metalnessSource, roughness
|
|
|
2183
707
|
return void 0;
|
|
2184
708
|
}
|
|
2185
709
|
const imageData = context.createImageData(width, height);
|
|
2186
|
-
const metalDefault = Math.round(
|
|
2187
|
-
const roughDefault = Math.round(
|
|
710
|
+
const metalDefault = Math.round(clamp01(metalnessFactor) * 255);
|
|
711
|
+
const roughDefault = Math.round(clamp01(roughnessFactor) * 255);
|
|
2188
712
|
for (let index = 0; index < imageData.data.length; index += 4) {
|
|
2189
713
|
imageData.data[index] = 0;
|
|
2190
714
|
imageData.data[index + 1] = roughness?.pixels[index] ?? roughDefault;
|
|
@@ -2194,9 +718,9 @@ async function createMetallicRoughnessTextureDataUri2(metalnessSource, roughness
|
|
|
2194
718
|
context.putImageData(imageData, 0, 0);
|
|
2195
719
|
const blob = await canvas.convertToBlob({ type: "image/png" });
|
|
2196
720
|
const buffer = await blob.arrayBuffer();
|
|
2197
|
-
return `data:image/png;base64,${
|
|
721
|
+
return `data:image/png;base64,${toBase64(new Uint8Array(buffer))}`;
|
|
2198
722
|
}
|
|
2199
|
-
async function
|
|
723
|
+
async function loadImagePixels(source) {
|
|
2200
724
|
if (!source || typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {
|
|
2201
725
|
return void 0;
|
|
2202
726
|
}
|
|
@@ -2250,11 +774,11 @@ function computeCylinderUvs(positions) {
|
|
|
2250
774
|
}
|
|
2251
775
|
return uvs;
|
|
2252
776
|
}
|
|
2253
|
-
function
|
|
777
|
+
function clamp01(value) {
|
|
2254
778
|
return Math.max(0, Math.min(1, value));
|
|
2255
779
|
}
|
|
2256
780
|
function toQuaternion(rotation) {
|
|
2257
|
-
const quaternion = new
|
|
781
|
+
const quaternion = new Quaternion().setFromEuler(new Euler(rotation.x, rotation.y, rotation.z, "XYZ"));
|
|
2258
782
|
return [quaternion.x, quaternion.y, quaternion.z, quaternion.w];
|
|
2259
783
|
}
|
|
2260
784
|
function createCylinderPrimitive() {
|
|
@@ -2292,7 +816,7 @@ function computePositionBounds(positions) {
|
|
|
2292
816
|
}
|
|
2293
817
|
return { max, min };
|
|
2294
818
|
}
|
|
2295
|
-
function
|
|
819
|
+
function toBase64(bytes) {
|
|
2296
820
|
let binary = "";
|
|
2297
821
|
bytes.forEach((byte) => {
|
|
2298
822
|
binary += String.fromCharCode(byte);
|