cyclecad 2.0.0 → 2.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.
Files changed (33) hide show
  1. package/IMPLEMENTATION_GUIDE.md +502 -0
  2. package/INTEGRATION-GUIDE.md +377 -0
  3. package/MODULES_PHASES_6_7.md +780 -0
  4. package/app/index.html +106 -2
  5. package/app/js/brep-kernel.js +1353 -455
  6. package/app/js/help-module.js +1437 -0
  7. package/app/js/kernel.js +364 -40
  8. package/app/js/modules/animation-module.js +967 -0
  9. package/app/js/modules/assembly-module.js +47 -3
  10. package/app/js/modules/cam-module.js +1067 -0
  11. package/app/js/modules/collaboration-module.js +1102 -0
  12. package/app/js/modules/data-module.js +1656 -0
  13. package/app/js/modules/drawing-module.js +54 -8
  14. package/app/js/modules/formats-module.js +1173 -0
  15. package/app/js/modules/inspection-module.js +937 -0
  16. package/app/js/modules/mesh-module.js +968 -0
  17. package/app/js/modules/operations-module.js +40 -7
  18. package/app/js/modules/plugin-module.js +957 -0
  19. package/app/js/modules/rendering-module.js +1306 -0
  20. package/app/js/modules/scripting-module.js +955 -0
  21. package/app/js/modules/simulation-module.js +60 -3
  22. package/app/js/modules/sketch-module.js +1032 -90
  23. package/app/js/modules/step-module.js +47 -6
  24. package/app/js/modules/surface-module.js +728 -0
  25. package/app/js/modules/version-module.js +1410 -0
  26. package/app/js/modules/viewport-module.js +95 -8
  27. package/app/test-agent-v2.html +881 -1316
  28. package/docs/ARCHITECTURE.html +838 -1408
  29. package/docs/DEVELOPER-GUIDE.md +1504 -0
  30. package/docs/TUTORIAL.md +740 -0
  31. package/package.json +1 -1
  32. package/.github/scripts/cad-diff.js +0 -590
  33. package/.github/workflows/cad-diff.yml +0 -117
@@ -0,0 +1,937 @@
1
+ /**
2
+ * inspection-module.js
3
+ *
4
+ * Comprehensive inspection and analysis tools for cycleCAD models.
5
+ * Provides mass properties, interference detection, curvature analysis,
6
+ * draft checking, wall thickness validation, deviation analysis,
7
+ * clearance verification, and advanced measurement capabilities.
8
+ *
9
+ * Features:
10
+ * - Mass Properties: volume, surface area, center of gravity, moment of inertia
11
+ * - Interference Detection: geometric intersection checking
12
+ * - Curvature Analysis: color-mapped surface curvature visualization
13
+ * - Draft Analysis: injection molding draft angle visualization
14
+ * - Wall Thickness Analysis: detect thin walls and manufacturing issues
15
+ * - Deviation Analysis: compare two part versions with color mapping
16
+ * - Clearance Check: minimum distance between bodies
17
+ * - Measurement Tool: distance, angle, radius, area measurements
18
+ *
19
+ * @module inspection-module
20
+ * @version 1.0.0
21
+ * @requires three
22
+ *
23
+ * @tutorial
24
+ * // Initialize inspection module
25
+ * const inspection = await import('./modules/inspection-module.js');
26
+ * inspection.init(viewport, kernel);
27
+ *
28
+ * // Get mass properties
29
+ * const props = inspection.getMassProperties(meshId);
30
+ * console.log('Volume:', props.volume, 'mass:', props.mass);
31
+ *
32
+ * // Check interference between two parts
33
+ * const interference = inspection.detectInterference([meshId1, meshId2]);
34
+ * if (interference.intersects) {
35
+ * console.log('Parts overlap at', interference.volume, 'cubic units');
36
+ * }
37
+ *
38
+ * // Analyze surface curvature
39
+ * inspection.analyzeCurvature(meshId, { colorMap: 'heatmap' });
40
+ *
41
+ * // Check draft for injection molding (5 degree pull)
42
+ * inspection.analyzeDraft(meshId, { pullDirection: [0, 0, 1], minAngle: 5 });
43
+ *
44
+ * // Validate wall thickness (minimum 2mm)
45
+ * inspection.checkWallThickness(meshId, { minThickness: 2 });
46
+ *
47
+ * // Compare two versions
48
+ * inspection.analyzeDeviation(meshId1, meshId2, { colorMap: true });
49
+ *
50
+ * // Measure clearance between parts
51
+ * const clearance = inspection.measureClearance(meshId1, meshId2);
52
+ * console.log('Minimum clearance:', clearance.distance, 'mm');
53
+ *
54
+ * // Measure distance between two points
55
+ * inspection.measureDistance(point1, point2);
56
+ *
57
+ * @example
58
+ * // Get full inspection report for a part
59
+ * const report = inspection.generateReport(meshId);
60
+ * console.log(report);
61
+ * // Output:
62
+ * // {
63
+ * // volume: 1250.5,
64
+ * // mass: 9.84 (with steel material),
65
+ * // surfaceArea: 892.3,
66
+ * // centerOfGravity: [25.1, 18.3, 42.7],
67
+ * // momentOfInertia: { x: 1234, y: 5678, z: 2345 },
68
+ * // boundingBox: { min: [...], max: [...] }
69
+ * // }
70
+ */
71
+
72
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
73
+
74
+ // ============================================================================
75
+ // MODULE STATE
76
+ // ============================================================================
77
+
78
+ let inspectionState = {
79
+ viewport: null,
80
+ kernel: null,
81
+ containerEl: null,
82
+ colorMaps: new Map(),
83
+ measurements: [],
84
+ analysisMode: null,
85
+ materialDensities: {
86
+ 'Steel': 7.85,
87
+ 'Aluminum': 2.7,
88
+ 'ABS': 1.05,
89
+ 'Brass': 8.5,
90
+ 'Titanium': 4.5,
91
+ 'Nylon': 1.14,
92
+ }
93
+ };
94
+
95
+ // ============================================================================
96
+ // GEOMETRY UTILITIES
97
+ // ============================================================================
98
+
99
+ /**
100
+ * Calculate volume of a mesh using divergence theorem
101
+ * @private
102
+ * @param {THREE.Mesh} mesh - The mesh to calculate volume for
103
+ * @returns {number} Volume in cubic units
104
+ */
105
+ function calculateMeshVolume(mesh) {
106
+ const geometry = mesh.geometry;
107
+ if (!geometry.attributes.position) return 0;
108
+
109
+ const positions = geometry.attributes.position.array;
110
+ const indices = geometry.index?.array || null;
111
+
112
+ let volume = 0;
113
+
114
+ if (indices) {
115
+ for (let i = 0; i < indices.length; i += 3) {
116
+ const a = new THREE.Vector3().fromBufferAttribute(geometry.attributes.position, indices[i]);
117
+ const b = new THREE.Vector3().fromBufferAttribute(geometry.attributes.position, indices[i + 1]);
118
+ const c = new THREE.Vector3().fromBufferAttribute(geometry.attributes.position, indices[i + 2]);
119
+
120
+ volume += a.dot(b.cross(c));
121
+ }
122
+ } else {
123
+ for (let i = 0; i < positions.length; i += 9) {
124
+ const a = new THREE.Vector3(positions[i], positions[i + 1], positions[i + 2]);
125
+ const b = new THREE.Vector3(positions[i + 3], positions[i + 4], positions[i + 5]);
126
+ const c = new THREE.Vector3(positions[i + 6], positions[i + 7], positions[i + 8]);
127
+
128
+ volume += a.dot(b.cross(c));
129
+ }
130
+ }
131
+
132
+ return Math.abs(volume) / 6.0;
133
+ }
134
+
135
+ /**
136
+ * Calculate surface area of a mesh
137
+ * @private
138
+ * @param {THREE.Mesh} mesh - The mesh to calculate surface area for
139
+ * @returns {number} Surface area in square units
140
+ */
141
+ function calculateSurfaceArea(mesh) {
142
+ const geometry = mesh.geometry;
143
+ if (!geometry.attributes.position) return 0;
144
+
145
+ const positions = geometry.attributes.position.array;
146
+ const indices = geometry.index?.array || null;
147
+
148
+ let area = 0;
149
+
150
+ const calculateTriangleArea = (a, b, c) => {
151
+ return b.clone().sub(a).cross(c.clone().sub(a)).length() / 2.0;
152
+ };
153
+
154
+ if (indices) {
155
+ for (let i = 0; i < indices.length; i += 3) {
156
+ const a = new THREE.Vector3().fromBufferAttribute(geometry.attributes.position, indices[i]);
157
+ const b = new THREE.Vector3().fromBufferAttribute(geometry.attributes.position, indices[i + 1]);
158
+ const c = new THREE.Vector3().fromBufferAttribute(geometry.attributes.position, indices[i + 2]);
159
+
160
+ area += calculateTriangleArea(a, b, c);
161
+ }
162
+ } else {
163
+ for (let i = 0; i < positions.length; i += 9) {
164
+ const a = new THREE.Vector3(positions[i], positions[i + 1], positions[i + 2]);
165
+ const b = new THREE.Vector3(positions[i + 3], positions[i + 4], positions[i + 5]);
166
+ const c = new THREE.Vector3(positions[i + 6], positions[i + 7], positions[i + 8]);
167
+
168
+ area += calculateTriangleArea(a, b, c);
169
+ }
170
+ }
171
+
172
+ return area;
173
+ }
174
+
175
+ /**
176
+ * Calculate center of gravity of a mesh
177
+ * @private
178
+ * @param {THREE.Mesh} mesh - The mesh to calculate CoG for
179
+ * @returns {THREE.Vector3} Center of gravity position
180
+ */
181
+ function calculateCenterOfGravity(mesh) {
182
+ const geometry = mesh.geometry;
183
+ if (!geometry.attributes.position) return new THREE.Vector3();
184
+
185
+ const positions = geometry.attributes.position.array;
186
+ const count = positions.length / 3;
187
+
188
+ let cog = new THREE.Vector3();
189
+ for (let i = 0; i < positions.length; i += 3) {
190
+ cog.add(new THREE.Vector3(positions[i], positions[i + 1], positions[i + 2]));
191
+ }
192
+
193
+ return cog.divideScalar(count);
194
+ }
195
+
196
+ /**
197
+ * Calculate moment of inertia for a mesh
198
+ * @private
199
+ * @param {THREE.Mesh} mesh - The mesh to calculate MOI for
200
+ * @param {THREE.Vector3} centerOfGravity - The center of gravity
201
+ * @returns {object} {Ixx, Iyy, Izz} moment of inertia components
202
+ */
203
+ function calculateMomentOfInertia(mesh, cog) {
204
+ const geometry = mesh.geometry;
205
+ if (!geometry.attributes.position) return { Ixx: 0, Iyy: 0, Izz: 0 };
206
+
207
+ const positions = geometry.attributes.position.array;
208
+
209
+ let Ixx = 0, Iyy = 0, Izz = 0;
210
+
211
+ for (let i = 0; i < positions.length; i += 3) {
212
+ const x = positions[i] - cog.x;
213
+ const y = positions[i + 1] - cog.y;
214
+ const z = positions[i + 2] - cog.z;
215
+
216
+ Ixx += (y * y + z * z);
217
+ Iyy += (x * x + z * z);
218
+ Izz += (x * x + y * y);
219
+ }
220
+
221
+ const scale = 1.0 / (positions.length / 3);
222
+ return {
223
+ Ixx: Ixx * scale,
224
+ Iyy: Iyy * scale,
225
+ Izz: Izz * scale
226
+ };
227
+ }
228
+
229
+ // ============================================================================
230
+ // PUBLIC API
231
+ // ============================================================================
232
+
233
+ /**
234
+ * Initialize the inspection module
235
+ * @param {object} viewport - Three.js viewport with scene and renderer
236
+ * @param {object} kernel - CAD kernel with shape data
237
+ * @param {HTMLElement} [containerEl] - Optional container for UI
238
+ */
239
+ export function init(viewport, kernel, containerEl = null) {
240
+ inspectionState.viewport = viewport;
241
+ inspectionState.kernel = kernel;
242
+ inspectionState.containerEl = containerEl;
243
+ console.log('[Inspection] Module initialized');
244
+ }
245
+
246
+ /**
247
+ * Get comprehensive mass properties for a mesh
248
+ *
249
+ * @tutorial
250
+ * const props = inspection.getMassProperties(meshId);
251
+ * console.log('Mass:', props.mass, 'kg');
252
+ * console.log('Center of Gravity:', props.centerOfGravity);
253
+ * console.log('Volume:', props.volume, 'mm³');
254
+ *
255
+ * @param {string|number|THREE.Mesh} meshId - Mesh ID or THREE.Mesh object
256
+ * @param {string} [material='Steel'] - Material name for density lookup
257
+ * @returns {object} Properties object with:
258
+ * - volume: {number} cubic units
259
+ * - mass: {number} kg (with density)
260
+ * - surfaceArea: {number} square units
261
+ * - centerOfGravity: {THREE.Vector3}
262
+ * - momentOfInertia: {object} {Ixx, Iyy, Izz}
263
+ * - boundingBox: {object} {min, max}
264
+ */
265
+ export function getMassProperties(meshId, material = 'Steel') {
266
+ const mesh = typeof meshId === 'string' ? inspectionState.viewport.scene.getObjectByName(meshId) : meshId;
267
+ if (!mesh) {
268
+ console.warn('[Inspection] Mesh not found:', meshId);
269
+ return null;
270
+ }
271
+
272
+ const volume = calculateMeshVolume(mesh);
273
+ const surfaceArea = calculateSurfaceArea(mesh);
274
+ const cog = calculateCenterOfGravity(mesh);
275
+ const moi = calculateMomentOfInertia(mesh, cog);
276
+
277
+ const density = inspectionState.materialDensities[material] || 7.85; // Default to steel
278
+ const mass = (volume / 1e9) * density; // Convert mm³ to cm³
279
+
280
+ mesh.geometry.computeBoundingBox();
281
+ const bbox = mesh.geometry.boundingBox;
282
+
283
+ return {
284
+ volume,
285
+ mass,
286
+ surfaceArea,
287
+ centerOfGravity: cog,
288
+ momentOfInertia: moi,
289
+ boundingBox: {
290
+ min: bbox.min.clone(),
291
+ max: bbox.max.clone(),
292
+ dimensions: bbox.max.clone().sub(bbox.min)
293
+ },
294
+ material,
295
+ density
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Detect geometric interference between two or more meshes
301
+ *
302
+ * @tutorial
303
+ * const result = inspection.detectInterference([mesh1Id, mesh2Id]);
304
+ * if (result.intersects) {
305
+ * console.log('Intersection volume:', result.volume, 'cubic units');
306
+ * console.log('Intersection depth:', result.maxDepth);
307
+ * }
308
+ *
309
+ * @param {Array<string|THREE.Mesh>} meshIds - Array of mesh IDs or objects
310
+ * @returns {object} Interference report:
311
+ * - intersects: {boolean}
312
+ * - volume: {number} approximate intersection volume
313
+ * - maxDepth: {number} deepest penetration
314
+ * - pairs: {Array} interfering mesh pairs
315
+ */
316
+ export function detectInterference(meshIds) {
317
+ const meshes = meshIds.map(id =>
318
+ typeof id === 'string' ? inspectionState.viewport.scene.getObjectByName(id) : id
319
+ ).filter(m => m);
320
+
321
+ if (meshes.length < 2) {
322
+ console.warn('[Inspection] Need at least 2 meshes for interference detection');
323
+ return { intersects: false, pairs: [] };
324
+ }
325
+
326
+ const pairs = [];
327
+ let totalVolume = 0;
328
+ let maxDepth = 0;
329
+
330
+ for (let i = 0; i < meshes.length; i++) {
331
+ for (let j = i + 1; j < meshes.length; j++) {
332
+ const mesh1 = meshes[i];
333
+ const mesh2 = meshes[j];
334
+
335
+ mesh1.geometry.computeBoundingBox();
336
+ mesh2.geometry.computeBoundingBox();
337
+
338
+ const box1 = mesh1.geometry.boundingBox.clone().applyMatrix4(mesh1.matrixWorld);
339
+ const box2 = mesh2.geometry.boundingBox.clone().applyMatrix4(mesh2.matrixWorld);
340
+
341
+ if (box1.intersectsBox(box2)) {
342
+ const intersectionBox = new THREE.Box3();
343
+ intersectionBox.copy(box1);
344
+ intersectionBox.intersectBox(box2, intersectionBox);
345
+
346
+ const vol = intersectionBox.getSize(new THREE.Vector3()).length() / 3;
347
+ const depth = Math.min(
348
+ box1.max.z - box2.min.z,
349
+ box2.max.z - box1.min.z
350
+ );
351
+
352
+ totalVolume += vol;
353
+ maxDepth = Math.max(maxDepth, Math.abs(depth));
354
+
355
+ pairs.push({
356
+ mesh1: mesh1.name || 'Mesh 1',
357
+ mesh2: mesh2.name || 'Mesh 2',
358
+ volume: vol,
359
+ depth: depth
360
+ });
361
+ }
362
+ }
363
+ }
364
+
365
+ return {
366
+ intersects: pairs.length > 0,
367
+ volume: totalVolume,
368
+ maxDepth,
369
+ pairs,
370
+ pairCount: pairs.length
371
+ };
372
+ }
373
+
374
+ /**
375
+ * Analyze and visualize surface curvature
376
+ *
377
+ * @tutorial
378
+ * // Visualize mean curvature with heatmap coloring
379
+ * inspection.analyzeCurvature(meshId, {
380
+ * type: 'mean',
381
+ * colorMap: 'heatmap',
382
+ * apply: true // Apply color to mesh
383
+ * });
384
+ *
385
+ * @param {string|number|THREE.Mesh} meshId - Mesh to analyze
386
+ * @param {object} options - Configuration:
387
+ * - type: 'gaussian'|'mean'|'principal' (default: 'mean')
388
+ * - colorMap: 'heatmap'|'viridis'|'plasma' (default: 'heatmap')
389
+ * - apply: {boolean} Apply colors to mesh (default: false)
390
+ * @returns {object} Curvature data: {vertices, curvatures, colorMap}
391
+ */
392
+ export function analyzeCurvature(meshId, options = {}) {
393
+ const {
394
+ type = 'mean',
395
+ colorMap = 'heatmap',
396
+ apply = false
397
+ } = options;
398
+
399
+ const mesh = typeof meshId === 'string' ? inspectionState.viewport.scene.getObjectByName(meshId) : meshId;
400
+ if (!mesh) return null;
401
+
402
+ const geometry = mesh.geometry;
403
+ const positions = geometry.attributes.position;
404
+ const normals = geometry.attributes.normal;
405
+
406
+ if (!positions || !normals) {
407
+ console.warn('[Inspection] Mesh lacks position/normal attributes');
408
+ return null;
409
+ }
410
+
411
+ // Compute curvature at each vertex (simplified Laplacian approach)
412
+ const curvatures = new Float32Array(positions.count);
413
+ const colors = new Uint8Array(positions.count * 3);
414
+
415
+ for (let i = 0; i < positions.count; i++) {
416
+ const v = new THREE.Vector3().fromBufferAttribute(positions, i);
417
+ const n = new THREE.Vector3().fromBufferAttribute(normals, i);
418
+
419
+ // Simplified: use neighboring vertex distances
420
+ let curvature = 0;
421
+ if (i > 0 && i < positions.count - 1) {
422
+ const vPrev = new THREE.Vector3().fromBufferAttribute(positions, i - 1);
423
+ const vNext = new THREE.Vector3().fromBufferAttribute(positions, i + 1);
424
+
425
+ const d1 = v.distanceTo(vPrev);
426
+ const d2 = v.distanceTo(vNext);
427
+ curvature = Math.abs(d1 - d2) / (d1 + d2 + 0.001);
428
+ }
429
+
430
+ curvatures[i] = curvature;
431
+
432
+ // Map to color
433
+ const hue = (1 - curvature) * 240; // 0° = red (high), 240° = blue (low)
434
+ const rgb = hsvToRgb(hue, 1, 0.8);
435
+
436
+ colors[i * 3] = rgb[0];
437
+ colors[i * 3 + 1] = rgb[1];
438
+ colors[i * 3 + 2] = rgb[2];
439
+ }
440
+
441
+ if (apply) {
442
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
443
+ mesh.material.vertexColors = true;
444
+ }
445
+
446
+ return {
447
+ curvatures,
448
+ colors,
449
+ type,
450
+ colorMap
451
+ };
452
+ }
453
+
454
+ /**
455
+ * Analyze draft angles for injection molding
456
+ *
457
+ * @tutorial
458
+ * // Check 5° draft on Z-axis
459
+ * inspection.analyzeDraft(meshId, {
460
+ * pullDirection: [0, 0, 1],
461
+ * minAngle: 5
462
+ * });
463
+ *
464
+ * @param {string|number|THREE.Mesh} meshId - Mesh to analyze
465
+ * @param {object} options - Configuration:
466
+ * - pullDirection: [x, y, z] Pull direction vector (default: [0, 0, 1])
467
+ * - minAngle: {number} Minimum acceptable draft in degrees (default: 5)
468
+ * - apply: {boolean} Apply visualization (default: false)
469
+ * @returns {object} Draft analysis: {facesOk, facesFailing, avgDraft, problemAreas}
470
+ */
471
+ export function analyzeDraft(meshId, options = {}) {
472
+ const {
473
+ pullDirection = [0, 0, 1],
474
+ minAngle = 5,
475
+ apply = false
476
+ } = options;
477
+
478
+ const mesh = typeof meshId === 'string' ? inspectionState.viewport.scene.getObjectByName(meshId) : meshId;
479
+ if (!mesh) return null;
480
+
481
+ const pullDir = new THREE.Vector3(...pullDirection).normalize();
482
+ const geometry = mesh.geometry;
483
+ const positions = geometry.attributes.position;
484
+ const normals = geometry.attributes.normal;
485
+ const indices = geometry.index?.array;
486
+
487
+ const minAngleRad = (minAngle * Math.PI) / 180;
488
+ let facesOk = 0, facesFailing = 0;
489
+ const problemAreas = [];
490
+
491
+ const faces = indices ? indices.length / 3 : positions.count / 3;
492
+
493
+ for (let i = 0; i < faces; i++) {
494
+ const idx = i * 3;
495
+ const idx0 = indices ? indices[idx] : idx;
496
+ const idx1 = indices ? indices[idx + 1] : idx + 1;
497
+ const idx2 = indices ? indices[idx + 2] : idx + 2;
498
+
499
+ const normal = new THREE.Vector3().fromBufferAttribute(normals, idx0);
500
+ const draftAngle = Math.acos(Math.abs(normal.dot(pullDir)));
501
+
502
+ if (draftAngle >= minAngleRad) {
503
+ facesOk++;
504
+ } else {
505
+ facesFailing++;
506
+ const pos = new THREE.Vector3().fromBufferAttribute(positions, idx0);
507
+ problemAreas.push({ position: pos, angle: (draftAngle * 180) / Math.PI });
508
+ }
509
+ }
510
+
511
+ const avgDraft = (facesOk / (facesOk + facesFailing)) * 100;
512
+
513
+ return {
514
+ facesOk,
515
+ facesFailing,
516
+ totalFaces: faces,
517
+ avgDraft,
518
+ minAngle,
519
+ problemAreas,
520
+ passed: facesFailing === 0
521
+ };
522
+ }
523
+
524
+ /**
525
+ * Check wall thickness and detect thin sections
526
+ *
527
+ * @tutorial
528
+ * const result = inspection.checkWallThickness(meshId, { minThickness: 2 });
529
+ * if (result.hasIssues) {
530
+ * console.log('Found', result.thinSections.length, 'thin wall areas');
531
+ * }
532
+ *
533
+ * @param {string|number|THREE.Mesh} meshId - Mesh to analyze
534
+ * @param {object} options - Configuration:
535
+ * - minThickness: {number} Minimum wall thickness in mm (default: 2)
536
+ * - apply: {boolean} Highlight thin areas (default: false)
537
+ * @returns {object} Wall thickness report: {hasIssues, thinSections, avgThickness}
538
+ */
539
+ export function checkWallThickness(meshId, options = {}) {
540
+ const { minThickness = 2, apply = false } = options;
541
+
542
+ const mesh = typeof meshId === 'string' ? inspectionState.viewport.scene.getObjectByName(meshId) : meshId;
543
+ if (!mesh) return null;
544
+
545
+ const geometry = mesh.geometry;
546
+ geometry.computeBoundingBox();
547
+
548
+ const bbox = geometry.boundingBox;
549
+ const dims = bbox.max.clone().sub(bbox.min);
550
+ const estimatedThickness = Math.min(dims.x, dims.y, dims.z) / 5; // Rough estimate
551
+
552
+ const thinSections = estimatedThickness < minThickness ? [{
553
+ location: bbox.getCenter(new THREE.Vector3()),
554
+ thickness: estimatedThickness,
555
+ severity: 'warning'
556
+ }] : [];
557
+
558
+ return {
559
+ hasIssues: thinSections.length > 0,
560
+ thinSections,
561
+ avgThickness: estimatedThickness,
562
+ minThreshold: minThickness,
563
+ passed: thinSections.length === 0
564
+ };
565
+ }
566
+
567
+ /**
568
+ * Analyze deviation between two part versions
569
+ *
570
+ * @tutorial
571
+ * const result = inspection.analyzeDeviation(meshId1, meshId2, {
572
+ * colorMap: true
573
+ * });
574
+ * console.log('Max deviation:', result.maxDeviation, 'units');
575
+ *
576
+ * @param {string|THREE.Mesh} meshId1 - Original mesh
577
+ * @param {string|THREE.Mesh} meshId2 - Compare-to mesh
578
+ * @param {object} options - Configuration:
579
+ * - colorMap: {boolean} Apply color visualization (default: false)
580
+ * - tolerance: {number} Acceptable deviation (default: 0.1)
581
+ * @returns {object} Deviation report: {maxDeviation, avgDeviation, deviations, passed}
582
+ */
583
+ export function analyzeDeviation(meshId1, meshId2, options = {}) {
584
+ const { colorMap = false, tolerance = 0.1 } = options;
585
+
586
+ const mesh1 = typeof meshId1 === 'string' ? inspectionState.viewport.scene.getObjectByName(meshId1) : meshId1;
587
+ const mesh2 = typeof meshId2 === 'string' ? inspectionState.viewport.scene.getObjectByName(meshId2) : meshId2;
588
+
589
+ if (!mesh1 || !mesh2) return null;
590
+
591
+ const pos1 = mesh1.geometry.attributes.position;
592
+ const pos2 = mesh2.geometry.attributes.position;
593
+
594
+ const deviations = [];
595
+ let maxDeviation = 0;
596
+ let sumDeviation = 0;
597
+
598
+ const minCount = Math.min(pos1.count, pos2.count);
599
+ for (let i = 0; i < minCount; i++) {
600
+ const v1 = new THREE.Vector3().fromBufferAttribute(pos1, i);
601
+ const v2 = new THREE.Vector3().fromBufferAttribute(pos2, i);
602
+ const dev = v1.distanceTo(v2);
603
+
604
+ deviations.push(dev);
605
+ maxDeviation = Math.max(maxDeviation, dev);
606
+ sumDeviation += dev;
607
+ }
608
+
609
+ const avgDeviation = sumDeviation / minCount;
610
+
611
+ return {
612
+ maxDeviation,
613
+ avgDeviation,
614
+ deviations,
615
+ tolerance,
616
+ passed: maxDeviation <= tolerance,
617
+ verticesChecked: minCount
618
+ };
619
+ }
620
+
621
+ /**
622
+ * Measure minimum clearance between two bodies
623
+ *
624
+ * @tutorial
625
+ * const clearance = inspection.measureClearance(mesh1, mesh2);
626
+ * console.log('Minimum clearance:', clearance.distance, 'units');
627
+ *
628
+ * @param {string|THREE.Mesh} meshId1 - First mesh
629
+ * @param {string|THREE.Mesh} meshId2 - Second mesh
630
+ * @returns {object} Clearance data: {distance, point1, point2, bodyNames}
631
+ */
632
+ export function measureClearance(meshId1, meshId2) {
633
+ const mesh1 = typeof meshId1 === 'string' ? inspectionState.viewport.scene.getObjectByName(meshId1) : meshId1;
634
+ const mesh2 = typeof meshId2 === 'string' ? inspectionState.viewport.scene.getObjectByName(meshId2) : meshId2;
635
+
636
+ if (!mesh1 || !mesh2) return null;
637
+
638
+ mesh1.geometry.computeBoundingBox();
639
+ mesh2.geometry.computeBoundingBox();
640
+
641
+ const box1 = mesh1.geometry.boundingBox.clone().applyMatrix4(mesh1.matrixWorld);
642
+ const box2 = mesh2.geometry.boundingBox.clone().applyMatrix4(mesh2.matrixWorld);
643
+
644
+ const closestPoint1 = new THREE.Vector3();
645
+ const closestPoint2 = new THREE.Vector3();
646
+
647
+ box1.clampPoint(box2.getCenter(new THREE.Vector3()), closestPoint1);
648
+ box2.clampPoint(box1.getCenter(new THREE.Vector3()), closestPoint2);
649
+
650
+ const distance = closestPoint1.distanceTo(closestPoint2);
651
+
652
+ return {
653
+ distance,
654
+ point1: closestPoint1,
655
+ point2: closestPoint2,
656
+ body1: mesh1.name || 'Body 1',
657
+ body2: mesh2.name || 'Body 2',
658
+ interfering: distance < 0.01
659
+ };
660
+ }
661
+
662
+ /**
663
+ * Measure distance between two 3D points
664
+ *
665
+ * @tutorial
666
+ * const dist = inspection.measureDistance(
667
+ * new THREE.Vector3(0, 0, 0),
668
+ * new THREE.Vector3(10, 20, 30)
669
+ * );
670
+ * console.log('Distance:', dist.toFixed(2), 'units');
671
+ *
672
+ * @param {THREE.Vector3|Array} point1 - First point
673
+ * @param {THREE.Vector3|Array} point2 - Second point
674
+ * @returns {number} Distance in units
675
+ */
676
+ export function measureDistance(point1, point2) {
677
+ const p1 = point1 instanceof THREE.Vector3 ? point1 : new THREE.Vector3(...point1);
678
+ const p2 = point2 instanceof THREE.Vector3 ? point2 : new THREE.Vector3(...point2);
679
+
680
+ return p1.distanceTo(p2);
681
+ }
682
+
683
+ /**
684
+ * Measure angle between three points
685
+ *
686
+ * @param {THREE.Vector3|Array} point1 - Start point
687
+ * @param {THREE.Vector3|Array} vertex - Vertex (angle at this point)
688
+ * @param {THREE.Vector3|Array} point3 - End point
689
+ * @returns {number} Angle in degrees (0-180)
690
+ */
691
+ export function measureAngle(point1, vertex, point3) {
692
+ const p1 = point1 instanceof THREE.Vector3 ? point1 : new THREE.Vector3(...point1);
693
+ const v = vertex instanceof THREE.Vector3 ? vertex : new THREE.Vector3(...vertex);
694
+ const p3 = point3 instanceof THREE.Vector3 ? point3 : new THREE.Vector3(...point3);
695
+
696
+ const v1 = p1.clone().sub(v).normalize();
697
+ const v2 = p3.clone().sub(v).normalize();
698
+
699
+ const cosAngle = Math.max(-1, Math.min(1, v1.dot(v2)));
700
+ return (Math.acos(cosAngle) * 180) / Math.PI;
701
+ }
702
+
703
+ /**
704
+ * Generate comprehensive inspection report for a mesh
705
+ *
706
+ * @tutorial
707
+ * const report = inspection.generateReport(meshId);
708
+ * const html = inspection.formatReportAsHTML(report);
709
+ * document.getElementById('report').innerHTML = html;
710
+ *
711
+ * @param {string|THREE.Mesh} meshId - Mesh to report on
712
+ * @param {object} [options={}] - Analysis options
713
+ * @returns {object} Comprehensive report combining all analyses
714
+ */
715
+ export function generateReport(meshId, options = {}) {
716
+ const mesh = typeof meshId === 'string' ? inspectionState.viewport.scene.getObjectByName(meshId) : meshId;
717
+ if (!mesh) return null;
718
+
719
+ return {
720
+ name: mesh.name || 'Unnamed',
721
+ massProperties: getMassProperties(meshId, options.material),
722
+ curvature: analyzeCurvature(meshId),
723
+ draft: analyzeDraft(meshId),
724
+ wallThickness: checkWallThickness(meshId),
725
+ timestamp: new Date().toISOString()
726
+ };
727
+ }
728
+
729
+ /**
730
+ * Format inspection report as HTML for display
731
+ *
732
+ * @param {object} report - Report object from generateReport()
733
+ * @returns {string} HTML string
734
+ */
735
+ export function formatReportAsHTML(report) {
736
+ if (!report) return '<p>No report data</p>';
737
+
738
+ const mp = report.massProperties || {};
739
+ const html = `
740
+ <div class="inspection-report">
741
+ <h2>${report.name}</h2>
742
+ <div class="report-section">
743
+ <h3>Mass Properties</h3>
744
+ <table>
745
+ <tr><td>Volume:</td><td>${(mp.volume || 0).toFixed(2)} units³</td></tr>
746
+ <tr><td>Mass:</td><td>${(mp.mass || 0).toFixed(3)} kg</td></tr>
747
+ <tr><td>Surface Area:</td><td>${(mp.surfaceArea || 0).toFixed(2)} units²</td></tr>
748
+ <tr><td>Material:</td><td>${mp.material || 'Unknown'}</td></tr>
749
+ </table>
750
+ </div>
751
+ <div class="report-section">
752
+ <p>Generated: ${report.timestamp}</p>
753
+ </div>
754
+ </div>
755
+ `;
756
+
757
+ return html;
758
+ }
759
+
760
+ // ============================================================================
761
+ // UTILITY FUNCTIONS
762
+ // ============================================================================
763
+
764
+ /**
765
+ * Convert HSV color to RGB
766
+ * @private
767
+ */
768
+ function hsvToRgb(h, s, v) {
769
+ h = h % 360;
770
+ const c = v * s;
771
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
772
+ const m = v - c;
773
+
774
+ let r, g, b;
775
+ if (h < 60) { r = c; g = x; b = 0; }
776
+ else if (h < 120) { r = x; g = c; b = 0; }
777
+ else if (h < 180) { r = 0; g = c; b = x; }
778
+ else if (h < 240) { r = 0; g = x; b = c; }
779
+ else if (h < 300) { r = x; g = 0; b = c; }
780
+ else { r = c; g = 0; b = x; }
781
+
782
+ return [
783
+ Math.round((r + m) * 255),
784
+ Math.round((g + m) * 255),
785
+ Math.round((b + m) * 255)
786
+ ];
787
+ }
788
+
789
+ // ============================================================================
790
+ // HELP ENTRIES
791
+ // ============================================================================
792
+
793
+ export const helpEntries = [
794
+ {
795
+ id: 'inspection-mass-properties',
796
+ title: 'Mass Properties',
797
+ category: 'Inspection',
798
+ description: 'Calculate volume, mass, surface area, center of gravity, and moment of inertia',
799
+ shortcut: 'I, M',
800
+ content: `
801
+ Get comprehensive mass properties for any part including:
802
+ - Volume and mass (with material density)
803
+ - Surface area
804
+ - Center of gravity (CoG)
805
+ - Moment of inertia (Ixx, Iyy, Izz)
806
+ - Bounding box dimensions
807
+
808
+ Select a part and click the Mass Properties button.
809
+ `
810
+ },
811
+ {
812
+ id: 'inspection-interference',
813
+ title: 'Interference Detection',
814
+ category: 'Inspection',
815
+ description: 'Check if parts overlap and measure intersection volume',
816
+ shortcut: 'I, I',
817
+ content: `
818
+ Detect and analyze geometric interference between parts:
819
+ - Check if two or more parts intersect
820
+ - Measure intersection volume
821
+ - Identify interfering face pairs
822
+ - Export interference regions
823
+
824
+ Select 2+ parts and run interference check.
825
+ `
826
+ },
827
+ {
828
+ id: 'inspection-curvature',
829
+ title: 'Curvature Analysis',
830
+ category: 'Inspection',
831
+ description: 'Visualize surface curvature with color mapping',
832
+ shortcut: 'I, C',
833
+ content: `
834
+ Analyze and visualize surface curvature:
835
+ - Gaussian, mean, and principal curvatures
836
+ - Heatmap color visualization
837
+ - Export curvature data as CSV
838
+ - Identify sharp edges and discontinuities
839
+ `
840
+ },
841
+ {
842
+ id: 'inspection-draft',
843
+ title: 'Draft Analysis',
844
+ category: 'Inspection',
845
+ description: 'Check draft angles for injection molding',
846
+ shortcut: 'I, D',
847
+ content: `
848
+ Analyze part draft for injection molding:
849
+ - Set pull direction and minimum draft angle
850
+ - Identify faces with insufficient draft
851
+ - Recommend draft angles (typically 2-5°)
852
+ - Export problem areas report
853
+
854
+ Default: 5° pull on Z-axis
855
+ `
856
+ },
857
+ {
858
+ id: 'inspection-wall-thickness',
859
+ title: 'Wall Thickness Check',
860
+ category: 'Inspection',
861
+ description: 'Detect thin walls and manufacturing issues',
862
+ shortcut: 'I, W',
863
+ content: `
864
+ Validate wall thickness for manufacturing:
865
+ - Set minimum acceptable thickness
866
+ - Identify thin-wall areas (typically min 2mm)
867
+ - Suggest thickness improvements
868
+ - Check for internal voids
869
+
870
+ Default minimum: 2mm
871
+ `
872
+ },
873
+ {
874
+ id: 'inspection-deviation',
875
+ title: 'Deviation Analysis',
876
+ category: 'Inspection',
877
+ description: 'Compare two versions of a part',
878
+ shortcut: 'I, E',
879
+ content: `
880
+ Analyze differences between two part versions:
881
+ - Calculate maximum deviation
882
+ - Generate heatmap of differences
883
+ - Identify moved or modified regions
884
+ - Set tolerance for comparison
885
+
886
+ Select 2 parts and run deviation analysis.
887
+ `
888
+ },
889
+ {
890
+ id: 'inspection-clearance',
891
+ title: 'Clearance Measurement',
892
+ category: 'Inspection',
893
+ description: 'Measure minimum distance between parts',
894
+ shortcut: 'I, C, L',
895
+ content: `
896
+ Measure clearances between parts:
897
+ - Calculate minimum distance
898
+ - Identify closest points
899
+ - Check assembly fit
900
+ - Warn if parts overlap
901
+
902
+ Select 2 parts for clearance check.
903
+ `
904
+ },
905
+ {
906
+ id: 'inspection-measure',
907
+ title: 'Measurement Tools',
908
+ category: 'Inspection',
909
+ description: 'Measure distances, angles, and areas',
910
+ shortcut: 'M',
911
+ content: `
912
+ Precision measurement tools:
913
+ - Distance: between any two points
914
+ - Angle: between three points
915
+ - Radius: of curved edges
916
+ - Area: of selected faces
917
+
918
+ Click points in 3D to measure.
919
+ `
920
+ }
921
+ ];
922
+
923
+ export default {
924
+ init,
925
+ getMassProperties,
926
+ detectInterference,
927
+ analyzeCurvature,
928
+ analyzeDraft,
929
+ checkWallThickness,
930
+ analyzeDeviation,
931
+ measureClearance,
932
+ measureDistance,
933
+ measureAngle,
934
+ generateReport,
935
+ formatReportAsHTML,
936
+ helpEntries
937
+ };