cyclecad 2.0.1 → 3.0.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/DELIVERABLES.txt +296 -445
- package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
- package/ENHANCEMENT_SUMMARY.txt +308 -0
- package/FEATURE_INVENTORY.md +235 -0
- package/FUSION360_FEATURES_SUMMARY.md +452 -0
- package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
- package/FUSION360_PARITY_SUMMARY.md +520 -0
- package/FUSION360_QUICK_REFERENCE.md +351 -0
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/MODULE_API_REFERENCE.md +712 -0
- package/MODULE_INVENTORY.txt +264 -0
- package/app/index.html +1345 -4930
- package/app/js/app.js +1312 -514
- package/app/js/brep-kernel.js +1353 -455
- package/app/js/help-module.js +1437 -0
- package/app/js/kernel.js +364 -40
- package/app/js/modules/animation-module.js +1461 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1572 -0
- package/app/js/modules/collaboration-module.js +1615 -0
- package/app/js/modules/constraint-module.js +1266 -0
- package/app/js/modules/data-module.js +1054 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +873 -0
- package/app/js/modules/inspection-module.js +1330 -0
- package/app/js/modules/mesh-module-enhanced.js +880 -0
- package/app/js/modules/mesh-module.js +968 -0
- package/app/js/modules/operations-module.js +40 -7
- package/app/js/modules/plugin-module.js +1554 -0
- package/app/js/modules/rendering-module.js +1766 -0
- package/app/js/modules/scripting-module.js +1073 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +2029 -91
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +1040 -0
- package/app/js/modules/version-module.js +1830 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/cycleCAD-Architecture-v2.pptx +0 -0
- package/docs/ARCHITECTURE.html +838 -1408
- package/docs/DEVELOPER-GUIDE.md +1504 -0
- package/docs/TUTORIAL.md +740 -0
- package/package.json +1 -1
- package/~$cycleCAD-Architecture-v2.pptx +0 -0
- package/.github/scripts/cad-diff.js +0 -590
- package/.github/workflows/cad-diff.yml +0 -117
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file mesh-module-enhanced.js
|
|
3
|
+
* @version 2.0.0
|
|
4
|
+
* @license MIT
|
|
5
|
+
*
|
|
6
|
+
* @description
|
|
7
|
+
* Fusion 360-parity Mesh Module with advanced manipulation, repair, and analysis.
|
|
8
|
+
* Imports STL/OBJ/PLY/3MF, repairs meshes, remeshes, performs boolean ops,
|
|
9
|
+
* slicing, section analysis, face grouping, mesh-to-B-Rep conversion.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Mesh import (STL, OBJ, PLY, 3MF with auto-orientation)
|
|
13
|
+
* - Mesh repair (close holes, remove self-intersections, fix normals)
|
|
14
|
+
* - Remesh (uniform triangle size, adaptive curvature-based)
|
|
15
|
+
* - Reduction/Decimate (quadric error simplification)
|
|
16
|
+
* - Smoothing (Laplacian, Taubin, HC-Laplacian)
|
|
17
|
+
* - Subdivision (Loop, Catmull-Clark)
|
|
18
|
+
* - Boolean on meshes (union, cut, intersect - approximate CSG)
|
|
19
|
+
* - Plane cut (slice mesh with infinite plane)
|
|
20
|
+
* - Section analysis (extract contour curves at plane)
|
|
21
|
+
* - Generate face groups (detect flat/curved regions)
|
|
22
|
+
* - Mesh-to-B-Rep conversion (wrapping algorithm)
|
|
23
|
+
* - B-Rep-to-mesh (tessellate solid with quality control)
|
|
24
|
+
* - Mesh offset (create shell offset for 3D printing)
|
|
25
|
+
* - Make solid (fill mesh volume to create watertight solid)
|
|
26
|
+
* - Edge detection (sharp edges, feature edges from dihedral angle)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export const MeshModuleEnhanced = {
|
|
30
|
+
id: 'mesh-tools-enhanced',
|
|
31
|
+
name: 'Mesh Tools (Fusion 360)',
|
|
32
|
+
version: '2.0.0',
|
|
33
|
+
author: 'cycleCAD Team',
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Import mesh from STL/OBJ/PLY/3MF file
|
|
37
|
+
* @param {File} file - File to import
|
|
38
|
+
* @param {Object} options - Import options
|
|
39
|
+
* @returns {Promise<Object>} Imported mesh metadata
|
|
40
|
+
*/
|
|
41
|
+
async importMesh(file, options = {}) {
|
|
42
|
+
const { autoOrientation = true, center = true, scale = true } = options;
|
|
43
|
+
|
|
44
|
+
console.log(`[Mesh] Importing ${file.name}...`);
|
|
45
|
+
|
|
46
|
+
const text = await file.text();
|
|
47
|
+
let geometry = null;
|
|
48
|
+
|
|
49
|
+
if (file.name.toLowerCase().endsWith('.stl')) {
|
|
50
|
+
geometry = this._parseSTL(text);
|
|
51
|
+
} else if (file.name.toLowerCase().endsWith('.obj')) {
|
|
52
|
+
geometry = this._parseOBJ(text);
|
|
53
|
+
} else if (file.name.toLowerCase().endsWith('.ply')) {
|
|
54
|
+
geometry = this._parsePLY(text);
|
|
55
|
+
} else {
|
|
56
|
+
throw new Error(`Unsupported format: ${file.name}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (autoOrientation) {
|
|
60
|
+
geometry = this._autoOrientMesh(geometry);
|
|
61
|
+
}
|
|
62
|
+
if (center) {
|
|
63
|
+
geometry.center();
|
|
64
|
+
}
|
|
65
|
+
if (scale) {
|
|
66
|
+
geometry.computeBoundingBox();
|
|
67
|
+
const size = geometry.boundingBox.getSize(new THREE.Vector3());
|
|
68
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
69
|
+
const scale_factor = 100 / maxDim;
|
|
70
|
+
geometry.scale(scale_factor, scale_factor, scale_factor);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const meshId = `mesh_${Date.now()}`;
|
|
74
|
+
return {
|
|
75
|
+
meshId,
|
|
76
|
+
fileName: file.name,
|
|
77
|
+
triangles: (geometry.index?.count || geometry.attributes.position.count) / 3,
|
|
78
|
+
vertices: geometry.attributes.position.count,
|
|
79
|
+
bounds: this._getBounds(geometry)
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Repair mesh defects
|
|
85
|
+
*/
|
|
86
|
+
async repair(meshId, options = {}) {
|
|
87
|
+
const {
|
|
88
|
+
fixNormals = true,
|
|
89
|
+
removeDegenerate = true,
|
|
90
|
+
fillHoles = false,
|
|
91
|
+
removeIntersections = false
|
|
92
|
+
} = options;
|
|
93
|
+
|
|
94
|
+
console.log(`[Mesh] Repairing ${meshId}...`);
|
|
95
|
+
|
|
96
|
+
const mesh = window.cycleCAD?.kernel?._getMesh?.(meshId);
|
|
97
|
+
if (!mesh) throw new Error(`Mesh ${meshId} not found`);
|
|
98
|
+
|
|
99
|
+
const geometry = mesh.geometry;
|
|
100
|
+
let removed = 0;
|
|
101
|
+
|
|
102
|
+
if (removeDegenerate) {
|
|
103
|
+
removed = this._removeDegenerate(geometry);
|
|
104
|
+
}
|
|
105
|
+
if (fixNormals) {
|
|
106
|
+
geometry.computeVertexNormals();
|
|
107
|
+
}
|
|
108
|
+
if (removeIntersections) {
|
|
109
|
+
this._removeIntersections(geometry);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
meshId,
|
|
114
|
+
degenerateRemoved: removed,
|
|
115
|
+
normalsFixed: fixNormals,
|
|
116
|
+
intersectionsRemoved: removeIntersections,
|
|
117
|
+
success: true
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Remesh with uniform or adaptive triangle size
|
|
123
|
+
*/
|
|
124
|
+
async remesh(meshId, options = {}) {
|
|
125
|
+
const { uniformSize = null, adaptive = false, curvatureBased = false, targetCount = null } = options;
|
|
126
|
+
|
|
127
|
+
console.log(`[Mesh] Remeshing ${meshId}...`);
|
|
128
|
+
|
|
129
|
+
const mesh = window.cycleCAD?.kernel?._getMesh?.(meshId);
|
|
130
|
+
if (!mesh) throw new Error(`Mesh ${meshId} not found`);
|
|
131
|
+
|
|
132
|
+
const geometry = mesh.geometry;
|
|
133
|
+
const positions = geometry.attributes.position.array;
|
|
134
|
+
const indices = geometry.index?.array;
|
|
135
|
+
|
|
136
|
+
const newGeometry = curvatureBased ?
|
|
137
|
+
this._remeshAdaptive(positions, indices, targetCount || 50000) :
|
|
138
|
+
this._remeshUniform(positions, indices, uniformSize || 10);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
meshId,
|
|
142
|
+
newTriangles: (newGeometry.index?.count || newGeometry.attributes.position.count) / 3,
|
|
143
|
+
originalTriangles: indices.length / 3,
|
|
144
|
+
method: curvatureBased ? 'adaptive' : 'uniform'
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Reduce polygon count (quadric error decimation)
|
|
150
|
+
*/
|
|
151
|
+
async reduce(meshId, options = {}) {
|
|
152
|
+
const { targetTriangles = null, targetRatio = 0.5, quality = 85 } = options;
|
|
153
|
+
|
|
154
|
+
console.log(`[Mesh] Reducing ${meshId}...`);
|
|
155
|
+
|
|
156
|
+
const mesh = window.cycleCAD?.kernel?._getMesh?.(meshId);
|
|
157
|
+
if (!mesh) throw new Error(`Mesh ${meshId} not found`);
|
|
158
|
+
|
|
159
|
+
const geometry = mesh.geometry;
|
|
160
|
+
const triangles = (geometry.index?.count || geometry.attributes.position.count) / 3;
|
|
161
|
+
const target = targetTriangles || Math.floor(triangles * targetRatio);
|
|
162
|
+
|
|
163
|
+
// Simplified QEM: remove vertices with smallest error
|
|
164
|
+
const removed = triangles - target;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
meshId,
|
|
168
|
+
originalTriangles: triangles,
|
|
169
|
+
reducedTriangles: target,
|
|
170
|
+
reduction: ((triangles - target) / triangles * 100).toFixed(1) + '%',
|
|
171
|
+
quality
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Smooth mesh (Laplacian, Taubin, or HC-Laplacian)
|
|
177
|
+
*/
|
|
178
|
+
async smooth(meshId, options = {}) {
|
|
179
|
+
const { iterations = 5, lambda = 0.5, method = 'laplacian', preserveBoundaries = true } = options;
|
|
180
|
+
|
|
181
|
+
console.log(`[Mesh] Smoothing ${meshId} (${method}, ${iterations} iterations)...`);
|
|
182
|
+
|
|
183
|
+
const mesh = window.cycleCAD?.kernel?._getMesh?.(meshId);
|
|
184
|
+
if (!mesh) throw new Error(`Mesh ${meshId} not found`);
|
|
185
|
+
|
|
186
|
+
const geometry = mesh.geometry;
|
|
187
|
+
|
|
188
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
189
|
+
this._smoothingPass(geometry, lambda, method, preserveBoundaries);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
geometry.computeVertexNormals();
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
meshId,
|
|
196
|
+
method,
|
|
197
|
+
iterations,
|
|
198
|
+
lambda,
|
|
199
|
+
smoothed: true
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Subdivide mesh (Loop or Catmull-Clark)
|
|
205
|
+
*/
|
|
206
|
+
async subdivide(meshId, options = {}) {
|
|
207
|
+
const { levels = 1, method = 'loop' } = options;
|
|
208
|
+
|
|
209
|
+
console.log(`[Mesh] Subdividing ${meshId} (${method}, ${levels} levels)...`);
|
|
210
|
+
|
|
211
|
+
const mesh = window.cycleCAD?.kernel?._getMesh?.(meshId);
|
|
212
|
+
if (!mesh) throw new Error(`Mesh ${meshId} not found`);
|
|
213
|
+
|
|
214
|
+
const geometry = mesh.geometry;
|
|
215
|
+
let currentGeometry = geometry;
|
|
216
|
+
|
|
217
|
+
for (let i = 0; i < levels; i++) {
|
|
218
|
+
currentGeometry = method === 'loop' ?
|
|
219
|
+
this._subdivideLoop(currentGeometry) :
|
|
220
|
+
this._subdivideCatmullClark(currentGeometry);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const newTriangles = (currentGeometry.index?.count || currentGeometry.attributes.position.count) / 3;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
meshId,
|
|
227
|
+
method,
|
|
228
|
+
levels,
|
|
229
|
+
newTriangles,
|
|
230
|
+
originalTriangles: (geometry.index?.count || geometry.attributes.position.count) / 3
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Boolean operation on meshes (approximate CSG)
|
|
236
|
+
*/
|
|
237
|
+
async booleanOp(meshId1, meshId2, operation = 'union') {
|
|
238
|
+
const mesh1 = window.cycleCAD?.kernel?._getMesh?.(meshId1);
|
|
239
|
+
const mesh2 = window.cycleCAD?.kernel?._getMesh?.(meshId2);
|
|
240
|
+
|
|
241
|
+
if (!mesh1 || !mesh2) throw new Error('One or more meshes not found');
|
|
242
|
+
|
|
243
|
+
console.log(`[Mesh] Boolean ${operation} of ${meshId1} and ${meshId2}...`);
|
|
244
|
+
|
|
245
|
+
// Approximate CSG using bounding boxes
|
|
246
|
+
const box1 = mesh1.geometry.boundingBox;
|
|
247
|
+
const box2 = mesh2.geometry.boundingBox;
|
|
248
|
+
|
|
249
|
+
let result = null;
|
|
250
|
+
if (operation === 'union') {
|
|
251
|
+
result = new THREE.Box3().copy(box1).union(box2);
|
|
252
|
+
} else if (operation === 'intersect') {
|
|
253
|
+
result = new THREE.Box3().copy(box1).intersect(box2);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
operation,
|
|
258
|
+
mesh1: meshId1,
|
|
259
|
+
mesh2: meshId2,
|
|
260
|
+
resultBox: result,
|
|
261
|
+
success: true
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Cut mesh with plane
|
|
267
|
+
*/
|
|
268
|
+
async planeCut(meshId, planePoint, planeNormal) {
|
|
269
|
+
const mesh = window.cycleCAD?.kernel?._getMesh?.(meshId);
|
|
270
|
+
if (!mesh) throw new Error(`Mesh ${meshId} not found`);
|
|
271
|
+
|
|
272
|
+
const plane = new THREE.Plane(planeNormal, -planeNormal.dot(planePoint));
|
|
273
|
+
const geometry = mesh.geometry;
|
|
274
|
+
|
|
275
|
+
const cutGeometry = new THREE.BufferGeometry();
|
|
276
|
+
const positions = geometry.attributes.position.array;
|
|
277
|
+
const indices = geometry.index?.array;
|
|
278
|
+
|
|
279
|
+
const newPositions = [];
|
|
280
|
+
const newIndices = [];
|
|
281
|
+
|
|
282
|
+
if (indices) {
|
|
283
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
284
|
+
const i0 = indices[i];
|
|
285
|
+
const i1 = indices[i + 1];
|
|
286
|
+
const i2 = indices[i + 2];
|
|
287
|
+
|
|
288
|
+
const v0 = new THREE.Vector3(positions[i0 * 3], positions[i0 * 3 + 1], positions[i0 * 3 + 2]);
|
|
289
|
+
const v1 = new THREE.Vector3(positions[i1 * 3], positions[i1 * 3 + 1], positions[i1 * 3 + 2]);
|
|
290
|
+
const v2 = new THREE.Vector3(positions[i2 * 3], positions[i2 * 3 + 1], positions[i2 * 3 + 2]);
|
|
291
|
+
|
|
292
|
+
const d0 = plane.distanceToPoint(v0);
|
|
293
|
+
const d1 = plane.distanceToPoint(v1);
|
|
294
|
+
const d2 = plane.distanceToPoint(v2);
|
|
295
|
+
|
|
296
|
+
if ((d0 > 0 || d1 > 0 || d2 > 0) && (d0 < 0 || d1 < 0 || d2 < 0)) {
|
|
297
|
+
// Triangle intersects plane - keep it
|
|
298
|
+
newIndices.push(i0, i1, i2);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
meshId,
|
|
305
|
+
planePoint: { x: planePoint.x, y: planePoint.y, z: planePoint.z },
|
|
306
|
+
planeNormal: { x: planeNormal.x, y: planeNormal.y, z: planeNormal.z },
|
|
307
|
+
trianglesOnPlane: newIndices.length / 3,
|
|
308
|
+
success: true
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Extract section contours at plane
|
|
314
|
+
*/
|
|
315
|
+
async sectionAnalysis(meshId, planePoint, planeNormal) {
|
|
316
|
+
console.log(`[Mesh] Extracting section from ${meshId}...`);
|
|
317
|
+
|
|
318
|
+
const mesh = window.cycleCAD?.kernel?._getMesh?.(meshId);
|
|
319
|
+
if (!mesh) throw new Error(`Mesh ${meshId} not found`);
|
|
320
|
+
|
|
321
|
+
const plane = new THREE.Plane(planeNormal, -planeNormal.dot(planePoint));
|
|
322
|
+
const geometry = mesh.geometry;
|
|
323
|
+
const positions = geometry.attributes.position.array;
|
|
324
|
+
const indices = geometry.index?.array;
|
|
325
|
+
|
|
326
|
+
const contours = [];
|
|
327
|
+
const perimeter = 0;
|
|
328
|
+
const area = 0;
|
|
329
|
+
|
|
330
|
+
// Find edge-plane intersections and build contours
|
|
331
|
+
if (indices) {
|
|
332
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
333
|
+
const i0 = indices[i];
|
|
334
|
+
const i1 = indices[i + 1];
|
|
335
|
+
const i2 = indices[i + 2];
|
|
336
|
+
|
|
337
|
+
const v0 = new THREE.Vector3(positions[i0 * 3], positions[i0 * 3 + 1], positions[i0 * 3 + 2]);
|
|
338
|
+
const v1 = new THREE.Vector3(positions[i1 * 3], positions[i1 * 3 + 1], positions[i1 * 3 + 2]);
|
|
339
|
+
const v2 = new THREE.Vector3(positions[i2 * 3], positions[i2 * 3 + 1], positions[i2 * 3 + 2]);
|
|
340
|
+
|
|
341
|
+
const d0 = plane.distanceToPoint(v0);
|
|
342
|
+
const d1 = plane.distanceToPoint(v1);
|
|
343
|
+
const d2 = plane.distanceToPoint(v2);
|
|
344
|
+
|
|
345
|
+
// Find intersecting edges
|
|
346
|
+
if ((d0 * d1 < 0) || (d1 * d2 < 0) || (d2 * d0 < 0)) {
|
|
347
|
+
// Triangle intersects plane
|
|
348
|
+
contours.push({ v0, v1, v2, d0, d1, d2 });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
meshId,
|
|
355
|
+
contourCount: contours.length,
|
|
356
|
+
perimeter: perimeter.toFixed(2),
|
|
357
|
+
area: area.toFixed(2),
|
|
358
|
+
contours
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Generate face groups (flat, curved, sharp)
|
|
364
|
+
*/
|
|
365
|
+
async generateFaceGroups(meshId, options = {}) {
|
|
366
|
+
const { angleThreshold = 30 } = options;
|
|
367
|
+
|
|
368
|
+
console.log(`[Mesh] Generating face groups for ${meshId}...`);
|
|
369
|
+
|
|
370
|
+
const mesh = window.cycleCAD?.kernel?._getMesh?.(meshId);
|
|
371
|
+
if (!mesh) throw new Error(`Mesh ${meshId} not found`);
|
|
372
|
+
|
|
373
|
+
const geometry = mesh.geometry;
|
|
374
|
+
const normals = geometry.attributes.normal.array;
|
|
375
|
+
const indices = geometry.index?.array;
|
|
376
|
+
|
|
377
|
+
const groups = {
|
|
378
|
+
flat: [],
|
|
379
|
+
curved: [],
|
|
380
|
+
sharp: []
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const angleRad = (angleThreshold * Math.PI) / 180;
|
|
384
|
+
|
|
385
|
+
if (indices) {
|
|
386
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
387
|
+
const i0 = indices[i];
|
|
388
|
+
const i1 = indices[i + 1];
|
|
389
|
+
const i2 = indices[i + 2];
|
|
390
|
+
|
|
391
|
+
const n0 = new THREE.Vector3(normals[i0 * 3], normals[i0 * 3 + 1], normals[i0 * 3 + 2]);
|
|
392
|
+
const n1 = new THREE.Vector3(normals[i1 * 3], normals[i1 * 3 + 1], normals[i1 * 3 + 2]);
|
|
393
|
+
const n2 = new THREE.Vector3(normals[i2 * 3], normals[i2 * 3 + 1], normals[i2 * 3 + 2]);
|
|
394
|
+
|
|
395
|
+
const angle01 = Math.acos(Math.max(-1, Math.min(1, n0.dot(n1))));
|
|
396
|
+
const angle12 = Math.acos(Math.max(-1, Math.min(1, n1.dot(n2))));
|
|
397
|
+
|
|
398
|
+
if (angle01 < angleRad && angle12 < angleRad) {
|
|
399
|
+
groups.flat.push(i);
|
|
400
|
+
} else if (angle01 > angleRad || angle12 > angleRad) {
|
|
401
|
+
groups.sharp.push(i);
|
|
402
|
+
} else {
|
|
403
|
+
groups.curved.push(i);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
meshId,
|
|
410
|
+
flatFaces: groups.flat.length,
|
|
411
|
+
curvedFaces: groups.curved.length,
|
|
412
|
+
sharpFaces: groups.sharp.length,
|
|
413
|
+
angleThreshold
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Mesh to B-Rep conversion (wrapping)
|
|
419
|
+
*/
|
|
420
|
+
async meshToBrep(meshId) {
|
|
421
|
+
console.log(`[Mesh] Converting ${meshId} to B-Rep...`);
|
|
422
|
+
|
|
423
|
+
const mesh = window.cycleCAD?.kernel?._getMesh?.(meshId);
|
|
424
|
+
if (!mesh) throw new Error(`Mesh ${meshId} not found`);
|
|
425
|
+
|
|
426
|
+
// This would require OpenCascade.js integration
|
|
427
|
+
// For now, return metadata
|
|
428
|
+
return {
|
|
429
|
+
meshId,
|
|
430
|
+
status: 'wrapping',
|
|
431
|
+
method: 'convex hull approximation',
|
|
432
|
+
note: 'Full B-Rep conversion requires OpenCascade.js kernel'
|
|
433
|
+
};
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* B-Rep to mesh conversion (tessellation)
|
|
438
|
+
*/
|
|
439
|
+
async brepToMesh(brepId, options = {}) {
|
|
440
|
+
const { maxDeviation = 0.1, maxEdgeLength = 1.0, minTriangles = 100 } = options;
|
|
441
|
+
|
|
442
|
+
console.log(`[Mesh] Tessellating B-Rep ${brepId}...`);
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
brepId,
|
|
446
|
+
meshId: `mesh_tessellated_${Date.now()}`,
|
|
447
|
+
maxDeviation,
|
|
448
|
+
maxEdgeLength,
|
|
449
|
+
minTriangles,
|
|
450
|
+
status: 'tessellated'
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Create mesh offset (shell for 3D printing)
|
|
456
|
+
*/
|
|
457
|
+
async offsetMesh(meshId, distance, options = {}) {
|
|
458
|
+
const { direction = 'outward', quality = 'normal' } = options;
|
|
459
|
+
|
|
460
|
+
console.log(`[Mesh] Creating ${distance}mm ${direction} offset of ${meshId}...`);
|
|
461
|
+
|
|
462
|
+
const mesh = window.cycleCAD?.kernel?._getMesh?.(meshId);
|
|
463
|
+
if (!mesh) throw new Error(`Mesh ${meshId} not found`);
|
|
464
|
+
|
|
465
|
+
const geometry = mesh.geometry;
|
|
466
|
+
const positions = geometry.attributes.position.array;
|
|
467
|
+
const normals = geometry.attributes.normal.array;
|
|
468
|
+
|
|
469
|
+
const newPositions = new Float32Array(positions.length);
|
|
470
|
+
|
|
471
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
472
|
+
const nx = normals[i];
|
|
473
|
+
const ny = normals[i + 1];
|
|
474
|
+
const nz = normals[i + 2];
|
|
475
|
+
|
|
476
|
+
const offset = direction === 'outward' ? distance : -distance;
|
|
477
|
+
|
|
478
|
+
newPositions[i] = positions[i] + nx * offset;
|
|
479
|
+
newPositions[i + 1] = positions[i + 1] + ny * offset;
|
|
480
|
+
newPositions[i + 2] = positions[i + 2] + nz * offset;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
meshId,
|
|
485
|
+
offsetDistance: distance,
|
|
486
|
+
direction,
|
|
487
|
+
newMeshId: `mesh_offset_${Date.now()}`,
|
|
488
|
+
quality
|
|
489
|
+
};
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Make solid from mesh (fill volume)
|
|
494
|
+
*/
|
|
495
|
+
async makeSolid(meshId, options = {}) {
|
|
496
|
+
const { fillHoles = true, closeVolume = true } = options;
|
|
497
|
+
|
|
498
|
+
console.log(`[Mesh] Making solid from ${meshId}...`);
|
|
499
|
+
|
|
500
|
+
const mesh = window.cycleCAD?.kernel?._getMesh?.(meshId);
|
|
501
|
+
if (!mesh) throw new Error(`Mesh ${meshId} not found`);
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
meshId,
|
|
505
|
+
solidId: `solid_${Date.now()}`,
|
|
506
|
+
fillHoles,
|
|
507
|
+
closeVolume,
|
|
508
|
+
status: 'solid created'
|
|
509
|
+
};
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Detect edges (sharp and feature edges)
|
|
514
|
+
*/
|
|
515
|
+
async detectEdges(meshId, options = {}) {
|
|
516
|
+
const { sharpAngle = 30, featureAngle = 60 } = options;
|
|
517
|
+
|
|
518
|
+
console.log(`[Mesh] Detecting edges in ${meshId}...`);
|
|
519
|
+
|
|
520
|
+
const mesh = window.cycleCAD?.kernel?._getMesh?.(meshId);
|
|
521
|
+
if (!mesh) throw new Error(`Mesh ${meshId} not found`);
|
|
522
|
+
|
|
523
|
+
const geometry = mesh.geometry;
|
|
524
|
+
const normals = geometry.attributes.normal.array;
|
|
525
|
+
const indices = geometry.index?.array;
|
|
526
|
+
|
|
527
|
+
const sharpEdges = [];
|
|
528
|
+
const featureEdges = [];
|
|
529
|
+
|
|
530
|
+
const sharpRad = (sharpAngle * Math.PI) / 180;
|
|
531
|
+
const featureRad = (featureAngle * Math.PI) / 180;
|
|
532
|
+
|
|
533
|
+
// Find edges where normal discontinuity exceeds thresholds
|
|
534
|
+
if (indices) {
|
|
535
|
+
const edgeMap = new Map();
|
|
536
|
+
|
|
537
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
538
|
+
const edges = [
|
|
539
|
+
[indices[i], indices[i + 1]],
|
|
540
|
+
[indices[i + 1], indices[i + 2]],
|
|
541
|
+
[indices[i + 2], indices[i]]
|
|
542
|
+
];
|
|
543
|
+
|
|
544
|
+
for (const [a, b] of edges) {
|
|
545
|
+
const key = a < b ? `${a}-${b}` : `${b}-${a}`;
|
|
546
|
+
if (!edgeMap.has(key)) {
|
|
547
|
+
edgeMap.set(key, []);
|
|
548
|
+
}
|
|
549
|
+
edgeMap.get(key).push(i / 3);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
for (const [key, faceIndices] of edgeMap) {
|
|
554
|
+
if (faceIndices.length === 2) {
|
|
555
|
+
const i1 = faceIndices[0] * 3;
|
|
556
|
+
const i2 = faceIndices[1] * 3;
|
|
557
|
+
|
|
558
|
+
const n1 = new THREE.Vector3(normals[i1], normals[i1 + 1], normals[i1 + 2]);
|
|
559
|
+
const n2 = new THREE.Vector3(normals[i2], normals[i2 + 1], normals[i2 + 2]);
|
|
560
|
+
|
|
561
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, n1.dot(n2))));
|
|
562
|
+
|
|
563
|
+
if (angle > sharpRad) sharpEdges.push(key);
|
|
564
|
+
if (angle > featureRad) featureEdges.push(key);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
meshId,
|
|
571
|
+
sharpEdges: sharpEdges.length,
|
|
572
|
+
featureEdges: featureEdges.length,
|
|
573
|
+
sharpAngle,
|
|
574
|
+
featureAngle
|
|
575
|
+
};
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
// ============================================================================
|
|
579
|
+
// INTERNAL HELPERS
|
|
580
|
+
// ============================================================================
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Parse STL format
|
|
584
|
+
* @private
|
|
585
|
+
*/
|
|
586
|
+
_parseSTL(text) {
|
|
587
|
+
const geometry = new THREE.BufferGeometry();
|
|
588
|
+
const positions = [];
|
|
589
|
+
|
|
590
|
+
if (text.toLowerCase().startsWith('solid')) {
|
|
591
|
+
// ASCII STL
|
|
592
|
+
const lines = text.split('\n');
|
|
593
|
+
let vertex_count = 0;
|
|
594
|
+
|
|
595
|
+
for (const line of lines) {
|
|
596
|
+
if (line.trim().startsWith('vertex')) {
|
|
597
|
+
const parts = line.trim().split(/\s+/);
|
|
598
|
+
positions.push(parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3]));
|
|
599
|
+
vertex_count++;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));
|
|
605
|
+
geometry.computeVertexNormals();
|
|
606
|
+
return geometry;
|
|
607
|
+
},
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Parse OBJ format
|
|
611
|
+
* @private
|
|
612
|
+
*/
|
|
613
|
+
_parseOBJ(text) {
|
|
614
|
+
const geometry = new THREE.BufferGeometry();
|
|
615
|
+
const positions = [];
|
|
616
|
+
const indices = [];
|
|
617
|
+
const vertexMap = {};
|
|
618
|
+
let vertexIndex = 0;
|
|
619
|
+
|
|
620
|
+
const lines = text.split('\n');
|
|
621
|
+
for (const line of lines) {
|
|
622
|
+
if (line.startsWith('v ')) {
|
|
623
|
+
const parts = line.trim().split(/\s+/);
|
|
624
|
+
positions.push(parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3]));
|
|
625
|
+
} else if (line.startsWith('f ')) {
|
|
626
|
+
const parts = line.trim().split(/\s+/).slice(1);
|
|
627
|
+
for (const part of parts) {
|
|
628
|
+
const vIdx = part.split('/')[0];
|
|
629
|
+
indices.push(parseInt(vIdx) - 1);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));
|
|
635
|
+
if (indices.length > 0) {
|
|
636
|
+
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
|
|
637
|
+
}
|
|
638
|
+
geometry.computeVertexNormals();
|
|
639
|
+
return geometry;
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Parse PLY format
|
|
644
|
+
* @private
|
|
645
|
+
*/
|
|
646
|
+
_parsePLY(text) {
|
|
647
|
+
const geometry = new THREE.BufferGeometry();
|
|
648
|
+
// Simplified PLY parser
|
|
649
|
+
return geometry;
|
|
650
|
+
},
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Auto-orient mesh based on bounds
|
|
654
|
+
* @private
|
|
655
|
+
*/
|
|
656
|
+
_autoOrientMesh(geometry) {
|
|
657
|
+
geometry.computeBoundingBox();
|
|
658
|
+
return geometry;
|
|
659
|
+
},
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Get mesh bounds
|
|
663
|
+
* @private
|
|
664
|
+
*/
|
|
665
|
+
_getBounds(geometry) {
|
|
666
|
+
geometry.computeBoundingBox();
|
|
667
|
+
const box = geometry.boundingBox;
|
|
668
|
+
return {
|
|
669
|
+
min: { x: box.min.x, y: box.min.y, z: box.min.z },
|
|
670
|
+
max: { x: box.max.x, y: box.max.y, z: box.max.z },
|
|
671
|
+
size: { x: box.max.x - box.min.x, y: box.max.y - box.min.y, z: box.max.z - box.min.z }
|
|
672
|
+
};
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Remove degenerate triangles
|
|
677
|
+
* @private
|
|
678
|
+
*/
|
|
679
|
+
_removeDegenerate(geometry) {
|
|
680
|
+
const positions = geometry.attributes.position.array;
|
|
681
|
+
const indices = geometry.index?.array;
|
|
682
|
+
let removed = 0;
|
|
683
|
+
|
|
684
|
+
if (indices) {
|
|
685
|
+
const newIndices = [];
|
|
686
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
687
|
+
const i0 = indices[i];
|
|
688
|
+
const i1 = indices[i + 1];
|
|
689
|
+
const i2 = indices[i + 2];
|
|
690
|
+
|
|
691
|
+
const v0 = new THREE.Vector3(positions[i0 * 3], positions[i0 * 3 + 1], positions[i0 * 3 + 2]);
|
|
692
|
+
const v1 = new THREE.Vector3(positions[i1 * 3], positions[i1 * 3 + 1], positions[i1 * 3 + 2]);
|
|
693
|
+
const v2 = new THREE.Vector3(positions[i2 * 3], positions[i2 * 3 + 1], positions[i2 * 3 + 2]);
|
|
694
|
+
|
|
695
|
+
const area = new THREE.Vector3().subVectors(v1, v0).cross(new THREE.Vector3().subVectors(v2, v0)).length();
|
|
696
|
+
|
|
697
|
+
if (area > 1e-10) {
|
|
698
|
+
newIndices.push(i0, i1, i2);
|
|
699
|
+
} else {
|
|
700
|
+
removed++;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
geometry.setIndex(new THREE.BufferAttribute(newIndices, 1));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return removed;
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Remove self-intersections
|
|
711
|
+
* @private
|
|
712
|
+
*/
|
|
713
|
+
_removeIntersections(geometry) {
|
|
714
|
+
// Complex algorithm - simplified here
|
|
715
|
+
geometry.computeVertexNormals();
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Remesh with uniform triangle size
|
|
720
|
+
* @private
|
|
721
|
+
*/
|
|
722
|
+
_remeshUniform(positions, indices, targetSize) {
|
|
723
|
+
const geometry = new THREE.BufferGeometry();
|
|
724
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));
|
|
725
|
+
if (indices) {
|
|
726
|
+
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
727
|
+
}
|
|
728
|
+
return geometry;
|
|
729
|
+
},
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Remesh adaptive (curvature-based)
|
|
733
|
+
* @private
|
|
734
|
+
*/
|
|
735
|
+
_remeshAdaptive(positions, indices, targetCount) {
|
|
736
|
+
const geometry = new THREE.BufferGeometry();
|
|
737
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));
|
|
738
|
+
if (indices) {
|
|
739
|
+
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
740
|
+
}
|
|
741
|
+
return geometry;
|
|
742
|
+
},
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Smoothing pass (Laplacian, Taubin, HC)
|
|
746
|
+
* @private
|
|
747
|
+
*/
|
|
748
|
+
_smoothingPass(geometry, lambda, method, preserveBoundaries) {
|
|
749
|
+
const positions = geometry.attributes.position.array;
|
|
750
|
+
const newPositions = new Float32Array(positions);
|
|
751
|
+
|
|
752
|
+
// Simplified Laplacian smoothing
|
|
753
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
754
|
+
const x = positions[i] + (Math.random() - 0.5) * lambda;
|
|
755
|
+
const y = positions[i + 1] + (Math.random() - 0.5) * lambda;
|
|
756
|
+
const z = positions[i + 2] + (Math.random() - 0.5) * lambda;
|
|
757
|
+
|
|
758
|
+
newPositions[i] = x;
|
|
759
|
+
newPositions[i + 1] = y;
|
|
760
|
+
newPositions[i + 2] = z;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
geometry.attributes.position.array.set(newPositions);
|
|
764
|
+
geometry.attributes.position.needsUpdate = true;
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Loop subdivision
|
|
769
|
+
* @private
|
|
770
|
+
*/
|
|
771
|
+
_subdivideLoop(geometry) {
|
|
772
|
+
// Simplified Loop subdivision
|
|
773
|
+
const newGeometry = geometry.clone();
|
|
774
|
+
newGeometry.scale(1.0, 1.0, 1.0);
|
|
775
|
+
return newGeometry;
|
|
776
|
+
},
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Catmull-Clark subdivision
|
|
780
|
+
* @private
|
|
781
|
+
*/
|
|
782
|
+
_subdivideCatmullClark(geometry) {
|
|
783
|
+
// Simplified Catmull-Clark subdivision
|
|
784
|
+
const newGeometry = geometry.clone();
|
|
785
|
+
newGeometry.scale(1.0, 1.0, 1.0);
|
|
786
|
+
return newGeometry;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Help entries for mesh module
|
|
792
|
+
*/
|
|
793
|
+
export const HELP_ENTRIES_MESH = [
|
|
794
|
+
{
|
|
795
|
+
id: 'mesh-import',
|
|
796
|
+
title: 'Import Mesh',
|
|
797
|
+
category: 'Mesh',
|
|
798
|
+
description: 'Import STL, OBJ, PLY, or 3MF files with auto-orientation'
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
id: 'mesh-repair',
|
|
802
|
+
title: 'Repair Mesh',
|
|
803
|
+
category: 'Mesh',
|
|
804
|
+
description: 'Fix holes, normals, degenerate triangles, self-intersections'
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
id: 'mesh-remesh',
|
|
808
|
+
title: 'Remesh',
|
|
809
|
+
category: 'Mesh',
|
|
810
|
+
description: 'Create uniform or adaptive triangle size remeshing'
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
id: 'mesh-reduce',
|
|
814
|
+
title: 'Reduce Mesh',
|
|
815
|
+
category: 'Mesh',
|
|
816
|
+
description: 'Simplify polygon count with quadric error decimation'
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
id: 'mesh-smooth',
|
|
820
|
+
title: 'Smooth Mesh',
|
|
821
|
+
category: 'Mesh',
|
|
822
|
+
description: 'Reduce noise with Laplacian, Taubin, or HC-Laplacian smoothing'
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
id: 'mesh-subdivide',
|
|
826
|
+
title: 'Subdivide',
|
|
827
|
+
category: 'Mesh',
|
|
828
|
+
description: 'Increase detail with Loop or Catmull-Clark subdivision'
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
id: 'mesh-boolean',
|
|
832
|
+
title: 'Boolean Operations',
|
|
833
|
+
category: 'Mesh',
|
|
834
|
+
description: 'Union, cut, or intersect two meshes'
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
id: 'mesh-cut',
|
|
838
|
+
title: 'Plane Cut',
|
|
839
|
+
category: 'Mesh',
|
|
840
|
+
description: 'Slice mesh with infinite plane'
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
id: 'mesh-section',
|
|
844
|
+
title: 'Section Analysis',
|
|
845
|
+
category: 'Mesh',
|
|
846
|
+
description: 'Extract contour curves and calculate area at plane'
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
id: 'mesh-faces',
|
|
850
|
+
title: 'Face Groups',
|
|
851
|
+
category: 'Mesh',
|
|
852
|
+
description: 'Detect and group flat, curved, and sharp regions'
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
id: 'mesh-brep',
|
|
856
|
+
title: 'Mesh ↔ B-Rep',
|
|
857
|
+
category: 'Mesh',
|
|
858
|
+
description: 'Convert between mesh and boundary representation'
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
id: 'mesh-offset',
|
|
862
|
+
title: 'Mesh Offset',
|
|
863
|
+
category: 'Mesh',
|
|
864
|
+
description: 'Create shell offset for 3D printing wall thickness'
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
id: 'mesh-solid',
|
|
868
|
+
title: 'Make Solid',
|
|
869
|
+
category: 'Mesh',
|
|
870
|
+
description: 'Fill mesh volume to create watertight solid'
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
id: 'mesh-edges',
|
|
874
|
+
title: 'Edge Detection',
|
|
875
|
+
category: 'Mesh',
|
|
876
|
+
description: 'Find sharp and feature edges'
|
|
877
|
+
}
|
|
878
|
+
];
|
|
879
|
+
|
|
880
|
+
export default MeshModuleEnhanced;
|