@womp/mesh-editor 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/README.md +33 -0
- package/dist/browser/src/mesh_editor/editor.d.ts +603 -0
- package/dist/browser/src/mesh_editor/editor.js +7297 -0
- package/dist/browser/src/mesh_editor/mesh_data.d.ts +14 -0
- package/dist/browser/src/mesh_editor/mesh_data.js +543 -0
- package/dist/browser/src/mesh_editor/sculpt_normal_worker.d.ts +36 -0
- package/dist/browser/src/mesh_editor/sculpt_normal_worker.js +83 -0
- package/dist/browser/src/mesh_editor/sculpt_normal_workers.d.ts +27 -0
- package/dist/browser/src/mesh_editor/sculpt_normal_workers.js +138 -0
- package/dist/browser/src/mesh_editor/three_geometry_adapter.d.ts +6 -0
- package/dist/browser/src/mesh_editor/three_geometry_adapter.js +334 -0
- package/dist/browser/src/mesh_editor/types.d.ts +44 -0
- package/dist/browser/src/mesh_editor/types.js +1 -0
- package/dist/browser/src/session/edit_session.d.ts +23 -0
- package/dist/browser/src/session/edit_session.js +1472 -0
- package/package.json +73 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EditableMaterial, EditableMesh, EditableMeshBounds, EditableMeshSourcePart, EditableTexture } from "./types.js";
|
|
2
|
+
export declare function create_default_editable_material(): EditableMaterial;
|
|
3
|
+
export declare function clone_editable_material(material: Partial<EditableMaterial> | undefined | null): EditableMaterial;
|
|
4
|
+
export declare function clone_editable_texture(texture: Partial<EditableTexture> | undefined | null): EditableTexture;
|
|
5
|
+
export declare function clone_editable_source_part(part: Partial<EditableMeshSourcePart> | undefined | null): EditableMeshSourcePart;
|
|
6
|
+
export declare function clone_editable_materials(materials: unknown, minimumCount: number): EditableMaterial[];
|
|
7
|
+
export declare function clone_editable_textures(textures: unknown): EditableTexture[];
|
|
8
|
+
export declare function clone_editable_source_parts(sourceParts: unknown): EditableMeshSourcePart[];
|
|
9
|
+
export declare function clone_editable_mesh(mesh: any): EditableMesh;
|
|
10
|
+
export declare function validate_editable_mesh(mesh: any): void;
|
|
11
|
+
export declare function editable_mesh_bounds(mesh: EditableMesh): EditableMeshBounds;
|
|
12
|
+
export declare function create_grid_fixture(size?: number, segments?: number): EditableMesh;
|
|
13
|
+
export declare function create_cube_fixture(size?: number): EditableMesh;
|
|
14
|
+
export declare function create_sphere_fixture(radius?: number, subdivisions?: number): EditableMesh;
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
export function create_default_editable_material() {
|
|
2
|
+
return {
|
|
3
|
+
baseColorTextureIndex: null,
|
|
4
|
+
baseColorFactor: [0.42, 0.42, 0.42, 1],
|
|
5
|
+
emissiveFactor: [0, 0, 0, 0],
|
|
6
|
+
roughnessFactor: 0.55,
|
|
7
|
+
metalnessFactor: 0,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function clone_vec4(value, fallback) {
|
|
11
|
+
const source = value;
|
|
12
|
+
if (source === undefined || source === null) {
|
|
13
|
+
return [fallback[0], fallback[1], fallback[2], fallback[3]];
|
|
14
|
+
}
|
|
15
|
+
return [
|
|
16
|
+
Number(source[0] ?? fallback[0]),
|
|
17
|
+
Number(source[1] ?? fallback[1]),
|
|
18
|
+
Number(source[2] ?? fallback[2]),
|
|
19
|
+
Number(source[3] ?? fallback[3]),
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
export function clone_editable_material(material) {
|
|
23
|
+
const fallback = create_default_editable_material();
|
|
24
|
+
if (material === undefined || material === null) {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
const textureIndex = material.baseColorTextureIndex;
|
|
28
|
+
return {
|
|
29
|
+
baseColorTextureIndex: textureIndex === null || textureIndex === undefined ? null : Number(textureIndex),
|
|
30
|
+
baseColorFactor: clone_vec4(material.baseColorFactor, fallback.baseColorFactor),
|
|
31
|
+
emissiveFactor: clone_vec4(material.emissiveFactor, fallback.emissiveFactor),
|
|
32
|
+
roughnessFactor: Number(material.roughnessFactor ?? fallback.roughnessFactor),
|
|
33
|
+
metalnessFactor: Number(material.metalnessFactor ?? fallback.metalnessFactor),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function clone_editable_texture(texture) {
|
|
37
|
+
const width = Number(texture?.width || 0);
|
|
38
|
+
const height = Number(texture?.height || 0);
|
|
39
|
+
const pixels = texture?.pixels instanceof Uint8Array ? new Uint8Array(texture.pixels) : new Uint8Array();
|
|
40
|
+
return { width, height, pixels };
|
|
41
|
+
}
|
|
42
|
+
function clone_matrix4(value) {
|
|
43
|
+
const source = value;
|
|
44
|
+
const output = [];
|
|
45
|
+
for (let column = 0; column < 4; column += 1) {
|
|
46
|
+
const sourceColumn = source?.[column];
|
|
47
|
+
output.push([
|
|
48
|
+
Number(sourceColumn?.[0] ?? (column === 0 ? 1 : 0)),
|
|
49
|
+
Number(sourceColumn?.[1] ?? (column === 1 ? 1 : 0)),
|
|
50
|
+
Number(sourceColumn?.[2] ?? (column === 2 ? 1 : 0)),
|
|
51
|
+
Number(sourceColumn?.[3] ?? (column === 3 ? 1 : 0)),
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
return output;
|
|
55
|
+
}
|
|
56
|
+
export function clone_editable_source_part(part) {
|
|
57
|
+
return {
|
|
58
|
+
name: String(part?.name || "mesh part"),
|
|
59
|
+
start: Number(part?.start || 0),
|
|
60
|
+
count: Number(part?.count || 0),
|
|
61
|
+
nodeTransform: clone_matrix4(part?.nodeTransform),
|
|
62
|
+
inverseNodeTransform: clone_matrix4(part?.inverseNodeTransform),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function clone_editable_materials(materials, minimumCount) {
|
|
66
|
+
const output = Array.from(materials || [], clone_editable_material);
|
|
67
|
+
while (output.length < Math.max(minimumCount, 1)) {
|
|
68
|
+
output.push(create_default_editable_material());
|
|
69
|
+
}
|
|
70
|
+
return output;
|
|
71
|
+
}
|
|
72
|
+
export function clone_editable_textures(textures) {
|
|
73
|
+
return Array.from(textures || [], clone_editable_texture);
|
|
74
|
+
}
|
|
75
|
+
export function clone_editable_source_parts(sourceParts) {
|
|
76
|
+
return Array.from(sourceParts || [], clone_editable_source_part);
|
|
77
|
+
}
|
|
78
|
+
export function clone_editable_mesh(mesh) {
|
|
79
|
+
const positions = new Float32Array(mesh.positions || []);
|
|
80
|
+
const normals = mesh.normals !== undefined && mesh.normals !== null
|
|
81
|
+
? new Float32Array(mesh.normals)
|
|
82
|
+
: null;
|
|
83
|
+
const indices = mesh.indices !== undefined && mesh.indices !== null
|
|
84
|
+
? new Uint32Array(mesh.indices)
|
|
85
|
+
: null;
|
|
86
|
+
const uvs = mesh.uvs !== undefined && mesh.uvs !== null ? new Float32Array(mesh.uvs) : null;
|
|
87
|
+
const colors = mesh.colors !== undefined && mesh.colors !== null
|
|
88
|
+
? new Float32Array(mesh.colors)
|
|
89
|
+
: null;
|
|
90
|
+
const materialGroups = Array.from(mesh.materialGroups || [], (group) => ({
|
|
91
|
+
start: Number(group.start || 0),
|
|
92
|
+
count: Number(group.count || 0),
|
|
93
|
+
materialIndex: Number(group.materialIndex || 0),
|
|
94
|
+
}));
|
|
95
|
+
const maxGroupMaterialIndex = materialGroups.reduce((maxIndex, group) => Math.max(maxIndex, group.materialIndex), 0);
|
|
96
|
+
const textures = clone_editable_textures(mesh.textures);
|
|
97
|
+
const materials = clone_editable_materials(mesh.materials, maxGroupMaterialIndex + 1);
|
|
98
|
+
const sourceParts = clone_editable_source_parts(mesh.sourceParts);
|
|
99
|
+
return {
|
|
100
|
+
name: String(mesh.name || "mesh"),
|
|
101
|
+
positions,
|
|
102
|
+
normals,
|
|
103
|
+
indices,
|
|
104
|
+
uvs,
|
|
105
|
+
colors,
|
|
106
|
+
materialGroups,
|
|
107
|
+
sourceParts,
|
|
108
|
+
materials,
|
|
109
|
+
textures,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function validate_range_partition(ranges, indexCount, label, requireOrdered) {
|
|
113
|
+
if (ranges.length === 0) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const ordered = Array.from(ranges);
|
|
117
|
+
if (requireOrdered === false) {
|
|
118
|
+
ordered.sort((a, b) => Number(a.start) - Number(b.start));
|
|
119
|
+
}
|
|
120
|
+
let expectedStart = 0;
|
|
121
|
+
for (const range of ordered) {
|
|
122
|
+
const start = Number(range.start);
|
|
123
|
+
const count = Number(range.count);
|
|
124
|
+
if (count <= 0 || start !== expectedStart) {
|
|
125
|
+
throw new Error(`${label} must form a complete, non-overlapping partition of the index buffer.`);
|
|
126
|
+
}
|
|
127
|
+
expectedStart += count;
|
|
128
|
+
}
|
|
129
|
+
if (expectedStart !== indexCount) {
|
|
130
|
+
throw new Error(`${label} must form a complete, non-overlapping partition of the index buffer.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function validate_source_part_transform(part) {
|
|
134
|
+
const matrices = [
|
|
135
|
+
{ value: part.nodeTransform, label: "nodeTransform" },
|
|
136
|
+
{ value: part.inverseNodeTransform, label: "inverseNodeTransform" },
|
|
137
|
+
];
|
|
138
|
+
for (const matrix of matrices) {
|
|
139
|
+
if (Array.isArray(matrix.value) === false || matrix.value.length !== 4) {
|
|
140
|
+
throw new Error(`Mesh source part ${matrix.label} must be a 4x4 matrix.`);
|
|
141
|
+
}
|
|
142
|
+
for (const column of matrix.value) {
|
|
143
|
+
if (Array.isArray(column) === false || column.length !== 4) {
|
|
144
|
+
throw new Error(`Mesh source part ${matrix.label} must be a 4x4 matrix.`);
|
|
145
|
+
}
|
|
146
|
+
for (const value of column) {
|
|
147
|
+
if (Number.isFinite(Number(value)) === false) {
|
|
148
|
+
throw new Error(`Mesh source part ${matrix.label} must contain finite values.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (let column = 0; column < 4; column += 1) {
|
|
154
|
+
for (let row = 0; row < 4; row += 1) {
|
|
155
|
+
let product = 0;
|
|
156
|
+
for (let inner = 0; inner < 4; inner += 1) {
|
|
157
|
+
product += part.nodeTransform[inner][row] * part.inverseNodeTransform[column][inner];
|
|
158
|
+
}
|
|
159
|
+
const expected = column === row ? 1 : 0;
|
|
160
|
+
if (Math.abs(product - expected) > 0.001) {
|
|
161
|
+
throw new Error("Mesh source part transforms must be inverses.");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export function validate_editable_mesh(mesh) {
|
|
167
|
+
if (mesh === undefined || mesh === null) {
|
|
168
|
+
throw new Error("Mesh is required.");
|
|
169
|
+
}
|
|
170
|
+
if ((mesh.positions instanceof Float32Array) === false) {
|
|
171
|
+
throw new Error("Mesh positions must be a Float32Array.");
|
|
172
|
+
}
|
|
173
|
+
if ((mesh.positions.length % 3) !== 0) {
|
|
174
|
+
throw new Error("Mesh positions length must be divisible by 3.");
|
|
175
|
+
}
|
|
176
|
+
const vertexCount = mesh.positions.length / 3;
|
|
177
|
+
for (let index = 0; index < mesh.positions.length; index += 1) {
|
|
178
|
+
if (Number.isFinite(mesh.positions[index]) === false) {
|
|
179
|
+
throw new Error("Mesh positions must contain finite values.");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (mesh.indices !== null && mesh.indices !== undefined) {
|
|
183
|
+
if ((mesh.indices instanceof Uint16Array) === false && (mesh.indices instanceof Uint32Array) === false) {
|
|
184
|
+
throw new Error("Mesh indices must be Uint16Array or Uint32Array.");
|
|
185
|
+
}
|
|
186
|
+
if ((mesh.indices.length % 3) !== 0) {
|
|
187
|
+
throw new Error("Mesh indices length must be divisible by 3.");
|
|
188
|
+
}
|
|
189
|
+
for (let index = 0; index < mesh.indices.length; index += 1) {
|
|
190
|
+
if (mesh.indices[index] >= vertexCount) {
|
|
191
|
+
throw new Error("Mesh indices contain an out-of-range vertex index.");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if ((vertexCount % 3) !== 0) {
|
|
196
|
+
throw new Error("Non-indexed mesh vertex count must be divisible by 3.");
|
|
197
|
+
}
|
|
198
|
+
if (mesh.uvs !== null && mesh.uvs !== undefined) {
|
|
199
|
+
if ((mesh.uvs instanceof Float32Array) === false || ((mesh.uvs.length % 2) !== 0)) {
|
|
200
|
+
throw new Error("Mesh uvs must be a Float32Array with vec2 elements.");
|
|
201
|
+
}
|
|
202
|
+
if ((mesh.uvs.length / 2) !== vertexCount) {
|
|
203
|
+
throw new Error("Mesh uv count must match position count.");
|
|
204
|
+
}
|
|
205
|
+
for (let index = 0; index < mesh.uvs.length; index += 1) {
|
|
206
|
+
if (Number.isFinite(mesh.uvs[index]) === false) {
|
|
207
|
+
throw new Error("Mesh uvs must contain finite values.");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (mesh.colors !== null && mesh.colors !== undefined) {
|
|
212
|
+
if ((mesh.colors instanceof Float32Array) === false || ((mesh.colors.length % 3) !== 0)) {
|
|
213
|
+
throw new Error("Mesh colors must be a Float32Array with vec3 elements.");
|
|
214
|
+
}
|
|
215
|
+
if ((mesh.colors.length / 3) !== vertexCount) {
|
|
216
|
+
throw new Error("Mesh color count must match position count.");
|
|
217
|
+
}
|
|
218
|
+
for (let index = 0; index < mesh.colors.length; index += 1) {
|
|
219
|
+
if (Number.isFinite(mesh.colors[index]) === false) {
|
|
220
|
+
throw new Error("Mesh colors must contain finite values.");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const materialGroups = mesh.materialGroups || [];
|
|
225
|
+
if (Array.isArray(materialGroups) === false) {
|
|
226
|
+
throw new Error("Mesh materialGroups must be an array.");
|
|
227
|
+
}
|
|
228
|
+
const indexCount = mesh.indices !== null && mesh.indices !== undefined ? mesh.indices.length : vertexCount;
|
|
229
|
+
for (const group of materialGroups) {
|
|
230
|
+
const start = Number(group.start);
|
|
231
|
+
const count = Number(group.count);
|
|
232
|
+
const materialIndex = Number(group.materialIndex);
|
|
233
|
+
if (Number.isInteger(start) === false ||
|
|
234
|
+
Number.isInteger(count) === false ||
|
|
235
|
+
Number.isInteger(materialIndex) === false ||
|
|
236
|
+
start < 0 ||
|
|
237
|
+
count < 0 ||
|
|
238
|
+
materialIndex < 0) {
|
|
239
|
+
throw new Error("Mesh material groups must contain non-negative integer fields.");
|
|
240
|
+
}
|
|
241
|
+
if (((start + count) > indexCount) || ((start % 3) !== 0) || ((count % 3) !== 0)) {
|
|
242
|
+
throw new Error("Mesh material group ranges must contain whole triangles inside the index buffer.");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (mesh.normals !== null && mesh.normals !== undefined) {
|
|
246
|
+
if ((mesh.normals instanceof Float32Array) === false || mesh.normals.length !== mesh.positions.length) {
|
|
247
|
+
throw new Error("Mesh normals must be a Float32Array matching position count.");
|
|
248
|
+
}
|
|
249
|
+
for (let index = 0; index < mesh.normals.length; index += 1) {
|
|
250
|
+
if (Number.isFinite(mesh.normals[index]) === false) {
|
|
251
|
+
throw new Error("Mesh normals must contain finite values.");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
validate_range_partition(materialGroups, indexCount, "Mesh material groups", false);
|
|
256
|
+
const sourceParts = mesh.sourceParts || [];
|
|
257
|
+
if (Array.isArray(sourceParts) === false) {
|
|
258
|
+
throw new Error("Mesh sourceParts must be an array.");
|
|
259
|
+
}
|
|
260
|
+
for (const part of sourceParts) {
|
|
261
|
+
const start = Number(part.start);
|
|
262
|
+
const count = Number(part.count);
|
|
263
|
+
if (Number.isInteger(start) === false || Number.isInteger(count) === false || start < 0 || count < 0) {
|
|
264
|
+
throw new Error("Mesh source parts must contain non-negative integer ranges.");
|
|
265
|
+
}
|
|
266
|
+
if (((start + count) > indexCount) || ((start % 3) !== 0) || ((count % 3) !== 0)) {
|
|
267
|
+
throw new Error("Mesh source part ranges must contain whole triangles inside the index buffer.");
|
|
268
|
+
}
|
|
269
|
+
validate_source_part_transform(part);
|
|
270
|
+
}
|
|
271
|
+
validate_range_partition(sourceParts, indexCount, "Mesh source parts", true);
|
|
272
|
+
const textures = mesh.textures || [];
|
|
273
|
+
if (Array.isArray(textures) === false) {
|
|
274
|
+
throw new Error("Mesh textures must be an array.");
|
|
275
|
+
}
|
|
276
|
+
for (const texture of textures) {
|
|
277
|
+
const width = Number(texture.width);
|
|
278
|
+
const height = Number(texture.height);
|
|
279
|
+
if (Number.isInteger(width) === false || Number.isInteger(height) === false || width <= 0 || height <= 0) {
|
|
280
|
+
throw new Error("Mesh textures must have positive integer dimensions.");
|
|
281
|
+
}
|
|
282
|
+
if ((texture.pixels instanceof Uint8Array) === false || texture.pixels.length !== width * height * 4) {
|
|
283
|
+
throw new Error("Mesh texture pixels must be RGBA8 data matching dimensions.");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const materials = mesh.materials || [];
|
|
287
|
+
if (Array.isArray(materials) === false) {
|
|
288
|
+
throw new Error("Mesh materials must be an array.");
|
|
289
|
+
}
|
|
290
|
+
for (const material of materials) {
|
|
291
|
+
const textureIndex = material.baseColorTextureIndex;
|
|
292
|
+
if (textureIndex !== null &&
|
|
293
|
+
textureIndex !== undefined &&
|
|
294
|
+
(Number.isInteger(Number(textureIndex)) === false || Number(textureIndex) < 0 || Number(textureIndex) >= textures.length)) {
|
|
295
|
+
throw new Error("Mesh material references an out-of-range texture.");
|
|
296
|
+
}
|
|
297
|
+
if (Array.isArray(material.baseColorFactor) === false || material.baseColorFactor.length !== 4) {
|
|
298
|
+
throw new Error("Mesh material base color factor must contain four values.");
|
|
299
|
+
}
|
|
300
|
+
for (const value of material.baseColorFactor) {
|
|
301
|
+
if (Number.isFinite(Number(value)) === false || Number(value) < 0 || Number(value) > 1) {
|
|
302
|
+
throw new Error("Mesh material base color factors must be finite values from zero to one.");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (Array.isArray(material.emissiveFactor) === false || material.emissiveFactor.length !== 4) {
|
|
306
|
+
throw new Error("Mesh material emissive factor must contain four values.");
|
|
307
|
+
}
|
|
308
|
+
for (const value of material.emissiveFactor) {
|
|
309
|
+
if (Number.isFinite(Number(value)) === false || Number(value) < 0 || Number(value) > 1) {
|
|
310
|
+
throw new Error("Mesh material emissive factors must be finite values from zero to one.");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (Number.isFinite(Number(material.roughnessFactor)) === false ||
|
|
314
|
+
Number(material.roughnessFactor) < 0 ||
|
|
315
|
+
Number(material.roughnessFactor) > 1 ||
|
|
316
|
+
Number.isFinite(Number(material.metalnessFactor)) === false ||
|
|
317
|
+
Number(material.metalnessFactor) < 0 ||
|
|
318
|
+
Number(material.metalnessFactor) > 1) {
|
|
319
|
+
throw new Error("Mesh material scalar factors must be finite values from zero to one.");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (const group of materialGroups) {
|
|
323
|
+
if (group.materialIndex >= materials.length) {
|
|
324
|
+
throw new Error("Mesh material group references an out-of-range material.");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
export function editable_mesh_bounds(mesh) {
|
|
329
|
+
validate_editable_mesh(mesh);
|
|
330
|
+
const positions = mesh.positions;
|
|
331
|
+
const min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
332
|
+
const max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
333
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
334
|
+
const x = positions[index + 0];
|
|
335
|
+
const y = positions[index + 1];
|
|
336
|
+
const z = positions[index + 2];
|
|
337
|
+
min[0] = Math.min(min[0], x);
|
|
338
|
+
min[1] = Math.min(min[1], y);
|
|
339
|
+
min[2] = Math.min(min[2], z);
|
|
340
|
+
max[0] = Math.max(max[0], x);
|
|
341
|
+
max[1] = Math.max(max[1], y);
|
|
342
|
+
max[2] = Math.max(max[2], z);
|
|
343
|
+
}
|
|
344
|
+
if (positions.length === 0) {
|
|
345
|
+
return { min: [0, 0, 0], max: [0, 0, 0], center: [0, 0, 0], radius: 1 };
|
|
346
|
+
}
|
|
347
|
+
const center = [
|
|
348
|
+
(min[0] + max[0]) * 0.5,
|
|
349
|
+
(min[1] + max[1]) * 0.5,
|
|
350
|
+
(min[2] + max[2]) * 0.5,
|
|
351
|
+
];
|
|
352
|
+
let radiusSq = 0;
|
|
353
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
354
|
+
const dx = positions[index + 0] - center[0];
|
|
355
|
+
const dy = positions[index + 1] - center[1];
|
|
356
|
+
const dz = positions[index + 2] - center[2];
|
|
357
|
+
radiusSq = Math.max(radiusSq, (dx * dx) + (dy * dy) + (dz * dz));
|
|
358
|
+
}
|
|
359
|
+
return { min, max, center, radius: Math.max(Math.sqrt(radiusSq), Number.MIN_VALUE) };
|
|
360
|
+
}
|
|
361
|
+
export function create_grid_fixture(size = 3, segments = 8) {
|
|
362
|
+
const vertexCount = (segments + 1) * (segments + 1);
|
|
363
|
+
const positions = new Float32Array(vertexCount * 3);
|
|
364
|
+
const uvs = new Float32Array(vertexCount * 2);
|
|
365
|
+
const colors = new Float32Array(vertexCount * 3);
|
|
366
|
+
let vertex = 0;
|
|
367
|
+
for (let y = 0; y <= segments; y += 1) {
|
|
368
|
+
for (let x = 0; x <= segments; x += 1) {
|
|
369
|
+
const u = x / segments;
|
|
370
|
+
const v = y / segments;
|
|
371
|
+
positions[(vertex * 3) + 0] = (u - 0.5) * size;
|
|
372
|
+
positions[(vertex * 3) + 1] = 0;
|
|
373
|
+
positions[(vertex * 3) + 2] = (v - 0.5) * size;
|
|
374
|
+
uvs[(vertex * 2) + 0] = u;
|
|
375
|
+
uvs[(vertex * 2) + 1] = v;
|
|
376
|
+
colors[(vertex * 3) + 0] = 1;
|
|
377
|
+
colors[(vertex * 3) + 1] = 1;
|
|
378
|
+
colors[(vertex * 3) + 2] = 1;
|
|
379
|
+
vertex += 1;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const indices = new Uint32Array(segments * segments * 6);
|
|
383
|
+
let offset = 0;
|
|
384
|
+
for (let y = 0; y < segments; y += 1) {
|
|
385
|
+
for (let x = 0; x < segments; x += 1) {
|
|
386
|
+
const a = x + (y * (segments + 1));
|
|
387
|
+
const b = a + 1;
|
|
388
|
+
const c = a + segments + 1;
|
|
389
|
+
const d = c + 1;
|
|
390
|
+
indices[offset + 0] = a;
|
|
391
|
+
indices[offset + 1] = c;
|
|
392
|
+
indices[offset + 2] = b;
|
|
393
|
+
indices[offset + 3] = b;
|
|
394
|
+
indices[offset + 4] = c;
|
|
395
|
+
indices[offset + 5] = d;
|
|
396
|
+
offset += 6;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
name: "grid",
|
|
401
|
+
positions,
|
|
402
|
+
normals: null,
|
|
403
|
+
indices,
|
|
404
|
+
uvs,
|
|
405
|
+
colors,
|
|
406
|
+
materialGroups: [{ start: 0, count: indices.length, materialIndex: 0 }],
|
|
407
|
+
sourceParts: [],
|
|
408
|
+
materials: [create_default_editable_material()],
|
|
409
|
+
textures: [],
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
export function create_cube_fixture(size = 2) {
|
|
413
|
+
const h = size * 0.5;
|
|
414
|
+
const positions = new Float32Array([
|
|
415
|
+
-h, -h, h, h, -h, h, h, h, h, -h, h, h,
|
|
416
|
+
h, -h, -h, -h, -h, -h, -h, h, -h, h, h, -h,
|
|
417
|
+
-h, h, h, h, h, h, h, h, -h, -h, h, -h,
|
|
418
|
+
-h, -h, -h, h, -h, -h, h, -h, h, -h, -h, h,
|
|
419
|
+
h, -h, h, h, -h, -h, h, h, -h, h, h, h,
|
|
420
|
+
-h, -h, -h, -h, -h, h, -h, h, h, -h, h, -h,
|
|
421
|
+
]);
|
|
422
|
+
const indices = new Uint32Array([
|
|
423
|
+
0, 1, 2, 0, 2, 3,
|
|
424
|
+
4, 5, 6, 4, 6, 7,
|
|
425
|
+
8, 9, 10, 8, 10, 11,
|
|
426
|
+
12, 13, 14, 12, 14, 15,
|
|
427
|
+
16, 17, 18, 16, 18, 19,
|
|
428
|
+
20, 21, 22, 20, 22, 23,
|
|
429
|
+
]);
|
|
430
|
+
const uvs = new Float32Array(24 * 2);
|
|
431
|
+
const colors = new Float32Array(24 * 3);
|
|
432
|
+
for (let index = 0; index < 24; index += 1) {
|
|
433
|
+
uvs[(index * 2) + 0] = index % 2;
|
|
434
|
+
uvs[(index * 2) + 1] = Math.floor(index / 2) % 2;
|
|
435
|
+
colors[(index * 3) + 0] = 1;
|
|
436
|
+
colors[(index * 3) + 1] = 1;
|
|
437
|
+
colors[(index * 3) + 2] = 1;
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
name: "cube",
|
|
441
|
+
positions,
|
|
442
|
+
normals: null,
|
|
443
|
+
indices,
|
|
444
|
+
uvs,
|
|
445
|
+
colors,
|
|
446
|
+
materialGroups: [{ start: 0, count: indices.length, materialIndex: 0 }],
|
|
447
|
+
sourceParts: [],
|
|
448
|
+
materials: [create_default_editable_material()],
|
|
449
|
+
textures: [],
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
export function create_sphere_fixture(radius = 1, subdivisions = 4) {
|
|
453
|
+
const t = (1 + Math.sqrt(5)) * 0.5;
|
|
454
|
+
const vertices = [
|
|
455
|
+
[-1, t, 0], [1, t, 0], [-1, -t, 0], [1, -t, 0],
|
|
456
|
+
[0, -1, t], [0, 1, t], [0, -1, -t], [0, 1, -t],
|
|
457
|
+
[t, 0, -1], [t, 0, 1], [-t, 0, -1], [-t, 0, 1],
|
|
458
|
+
];
|
|
459
|
+
let faces = [
|
|
460
|
+
[0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11],
|
|
461
|
+
[1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8],
|
|
462
|
+
[3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9],
|
|
463
|
+
[4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1],
|
|
464
|
+
];
|
|
465
|
+
function normalize_vertex(index) {
|
|
466
|
+
const vertex = vertices[index];
|
|
467
|
+
const length = Math.hypot(vertex[0], vertex[1], vertex[2]);
|
|
468
|
+
vertex[0] = (vertex[0] / length) * radius;
|
|
469
|
+
vertex[1] = (vertex[1] / length) * radius;
|
|
470
|
+
vertex[2] = (vertex[2] / length) * radius;
|
|
471
|
+
}
|
|
472
|
+
for (let index = 0; index < vertices.length; index += 1) {
|
|
473
|
+
normalize_vertex(index);
|
|
474
|
+
}
|
|
475
|
+
for (let level = 0; level < subdivisions; level += 1) {
|
|
476
|
+
const midpointCache = new Map();
|
|
477
|
+
const midpoint = (a, b) => {
|
|
478
|
+
const min = Math.min(a, b);
|
|
479
|
+
const max = Math.max(a, b);
|
|
480
|
+
const key = `${min}:${max}`;
|
|
481
|
+
const cached = midpointCache.get(key);
|
|
482
|
+
if (cached !== undefined) {
|
|
483
|
+
return cached;
|
|
484
|
+
}
|
|
485
|
+
const va = vertices[a];
|
|
486
|
+
const vb = vertices[b];
|
|
487
|
+
const index = vertices.length;
|
|
488
|
+
vertices.push([
|
|
489
|
+
(va[0] + vb[0]) * 0.5,
|
|
490
|
+
(va[1] + vb[1]) * 0.5,
|
|
491
|
+
(va[2] + vb[2]) * 0.5,
|
|
492
|
+
]);
|
|
493
|
+
normalize_vertex(index);
|
|
494
|
+
midpointCache.set(key, index);
|
|
495
|
+
return index;
|
|
496
|
+
};
|
|
497
|
+
const nextFaces = [];
|
|
498
|
+
for (const face of faces) {
|
|
499
|
+
const ab = midpoint(face[0], face[1]);
|
|
500
|
+
const bc = midpoint(face[1], face[2]);
|
|
501
|
+
const ca = midpoint(face[2], face[0]);
|
|
502
|
+
nextFaces.push([face[0], ab, ca], [face[1], bc, ab], [face[2], ca, bc], [ab, bc, ca]);
|
|
503
|
+
}
|
|
504
|
+
faces = nextFaces;
|
|
505
|
+
}
|
|
506
|
+
const positions = new Float32Array(vertices.length * 3);
|
|
507
|
+
const uvs = new Float32Array(vertices.length * 2);
|
|
508
|
+
const colors = new Float32Array(vertices.length * 3);
|
|
509
|
+
for (let index = 0; index < vertices.length; index += 1) {
|
|
510
|
+
const vertex = vertices[index];
|
|
511
|
+
const nx = vertex[0] / radius;
|
|
512
|
+
const ny = vertex[1] / radius;
|
|
513
|
+
const nz = vertex[2] / radius;
|
|
514
|
+
positions[(index * 3) + 0] = vertex[0];
|
|
515
|
+
positions[(index * 3) + 1] = vertex[1];
|
|
516
|
+
positions[(index * 3) + 2] = vertex[2];
|
|
517
|
+
uvs[(index * 2) + 0] = 0.5 + (Math.atan2(nz, nx) / (Math.PI * 2));
|
|
518
|
+
uvs[(index * 2) + 1] = 0.5 - (Math.asin(ny) / Math.PI);
|
|
519
|
+
colors[(index * 3) + 0] = 1;
|
|
520
|
+
colors[(index * 3) + 1] = 1;
|
|
521
|
+
colors[(index * 3) + 2] = 1;
|
|
522
|
+
}
|
|
523
|
+
const indices = new Uint32Array(faces.length * 3);
|
|
524
|
+
let offset = 0;
|
|
525
|
+
for (const face of faces) {
|
|
526
|
+
indices[offset + 0] = face[0];
|
|
527
|
+
indices[offset + 1] = face[1];
|
|
528
|
+
indices[offset + 2] = face[2];
|
|
529
|
+
offset += 3;
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
name: "icosphere",
|
|
533
|
+
positions,
|
|
534
|
+
normals: null,
|
|
535
|
+
indices,
|
|
536
|
+
uvs,
|
|
537
|
+
colors,
|
|
538
|
+
materialGroups: [{ start: 0, count: indices.length, materialIndex: 0 }],
|
|
539
|
+
sourceParts: [],
|
|
540
|
+
materials: [create_default_editable_material()],
|
|
541
|
+
textures: [],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type WorkerPhase = "triangles" | "vertices";
|
|
2
|
+
interface ConfigureMessage {
|
|
3
|
+
type: "configure";
|
|
4
|
+
positions: SharedArrayBuffer;
|
|
5
|
+
indices: SharedArrayBuffer | null;
|
|
6
|
+
indexBytes: number;
|
|
7
|
+
triangleOffsets: SharedArrayBuffer;
|
|
8
|
+
triangleIndices: SharedArrayBuffer;
|
|
9
|
+
triangleNormals: SharedArrayBuffer;
|
|
10
|
+
normals: SharedArrayBuffer;
|
|
11
|
+
taskTriangles: SharedArrayBuffer;
|
|
12
|
+
taskVertices: SharedArrayBuffer;
|
|
13
|
+
}
|
|
14
|
+
interface RunMessage {
|
|
15
|
+
type: "run";
|
|
16
|
+
taskId: number;
|
|
17
|
+
phase: WorkerPhase;
|
|
18
|
+
start: number;
|
|
19
|
+
end: number;
|
|
20
|
+
}
|
|
21
|
+
declare let positions: Float32Array<ArrayBufferLike>;
|
|
22
|
+
declare let indices: Uint16Array<ArrayBufferLike> | Uint32Array<ArrayBufferLike> | null;
|
|
23
|
+
declare let triangleOffsets: Uint32Array<ArrayBufferLike>;
|
|
24
|
+
declare let triangleIndices: Uint32Array<ArrayBufferLike>;
|
|
25
|
+
declare let triangleNormals: Float32Array<ArrayBufferLike>;
|
|
26
|
+
declare let normals: Float32Array<ArrayBufferLike>;
|
|
27
|
+
declare let taskTriangles: Uint32Array<ArrayBufferLike>;
|
|
28
|
+
declare let taskVertices: Uint32Array<ArrayBufferLike>;
|
|
29
|
+
declare function configure(message: ConfigureMessage): void;
|
|
30
|
+
declare function source_vertex(indexOffset: number): number;
|
|
31
|
+
declare function refresh_triangles(start: number, end: number): void;
|
|
32
|
+
declare function refresh_vertices(start: number, end: number): void;
|
|
33
|
+
declare const workerScope: {
|
|
34
|
+
onmessage: ((event: MessageEvent<ConfigureMessage | RunMessage>) => void) | null;
|
|
35
|
+
postMessage: (message: unknown) => void;
|
|
36
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
let positions = new Float32Array();
|
|
3
|
+
let indices = null;
|
|
4
|
+
let triangleOffsets = new Uint32Array();
|
|
5
|
+
let triangleIndices = new Uint32Array();
|
|
6
|
+
let triangleNormals = new Float32Array();
|
|
7
|
+
let normals = new Float32Array();
|
|
8
|
+
let taskTriangles = new Uint32Array();
|
|
9
|
+
let taskVertices = new Uint32Array();
|
|
10
|
+
function configure(message) {
|
|
11
|
+
positions = new Float32Array(message.positions);
|
|
12
|
+
indices = message.indices === null
|
|
13
|
+
? null
|
|
14
|
+
: message.indexBytes === Uint16Array.BYTES_PER_ELEMENT
|
|
15
|
+
? new Uint16Array(message.indices)
|
|
16
|
+
: new Uint32Array(message.indices);
|
|
17
|
+
triangleOffsets = new Uint32Array(message.triangleOffsets);
|
|
18
|
+
triangleIndices = new Uint32Array(message.triangleIndices);
|
|
19
|
+
triangleNormals = new Float32Array(message.triangleNormals);
|
|
20
|
+
normals = new Float32Array(message.normals);
|
|
21
|
+
taskTriangles = new Uint32Array(message.taskTriangles);
|
|
22
|
+
taskVertices = new Uint32Array(message.taskVertices);
|
|
23
|
+
}
|
|
24
|
+
function source_vertex(indexOffset) {
|
|
25
|
+
return indices === null ? indexOffset : indices[indexOffset];
|
|
26
|
+
}
|
|
27
|
+
function refresh_triangles(start, end) {
|
|
28
|
+
for (let taskIndex = start; taskIndex < end; taskIndex += 1) {
|
|
29
|
+
const triangleIndex = taskTriangles[taskIndex];
|
|
30
|
+
const triangleOffset = triangleIndex * 3;
|
|
31
|
+
const a = source_vertex(triangleOffset + 0) * 3;
|
|
32
|
+
const b = source_vertex(triangleOffset + 1) * 3;
|
|
33
|
+
const c = source_vertex(triangleOffset + 2) * 3;
|
|
34
|
+
const abX = positions[b + 0] - positions[a + 0];
|
|
35
|
+
const abY = positions[b + 1] - positions[a + 1];
|
|
36
|
+
const abZ = positions[b + 2] - positions[a + 2];
|
|
37
|
+
const acX = positions[c + 0] - positions[a + 0];
|
|
38
|
+
const acY = positions[c + 1] - positions[a + 1];
|
|
39
|
+
const acZ = positions[c + 2] - positions[a + 2];
|
|
40
|
+
triangleNormals[triangleOffset + 0] = (abY * acZ) - (abZ * acY);
|
|
41
|
+
triangleNormals[triangleOffset + 1] = (abZ * acX) - (abX * acZ);
|
|
42
|
+
triangleNormals[triangleOffset + 2] = (abX * acY) - (abY * acX);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function refresh_vertices(start, end) {
|
|
46
|
+
for (let taskIndex = start; taskIndex < end; taskIndex += 1) {
|
|
47
|
+
const vertexIndex = taskVertices[taskIndex];
|
|
48
|
+
let normalX = 0;
|
|
49
|
+
let normalY = 0;
|
|
50
|
+
let normalZ = 0;
|
|
51
|
+
const triangleStart = triangleOffsets[vertexIndex];
|
|
52
|
+
const triangleEnd = triangleOffsets[vertexIndex + 1];
|
|
53
|
+
for (let adjacencyIndex = triangleStart; adjacencyIndex < triangleEnd; adjacencyIndex += 1) {
|
|
54
|
+
const triangleOffset = triangleIndices[adjacencyIndex] * 3;
|
|
55
|
+
normalX += triangleNormals[triangleOffset + 0];
|
|
56
|
+
normalY += triangleNormals[triangleOffset + 1];
|
|
57
|
+
normalZ += triangleNormals[triangleOffset + 2];
|
|
58
|
+
}
|
|
59
|
+
const length = Math.hypot(normalX, normalY, normalZ);
|
|
60
|
+
if (length <= Number.MIN_VALUE) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const offset = vertexIndex * 3;
|
|
64
|
+
normals[offset + 0] = normalX / length;
|
|
65
|
+
normals[offset + 1] = normalY / length;
|
|
66
|
+
normals[offset + 2] = normalZ / length;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const workerScope = self;
|
|
70
|
+
workerScope.onmessage = (event) => {
|
|
71
|
+
const message = event.data;
|
|
72
|
+
if (message.type === "configure") {
|
|
73
|
+
configure(message);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (message.phase === "triangles") {
|
|
77
|
+
refresh_triangles(message.start, message.end);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
refresh_vertices(message.start, message.end);
|
|
81
|
+
}
|
|
82
|
+
workerScope.postMessage({ type: "done", taskId: message.taskId });
|
|
83
|
+
};
|