cyclecad 3.2.1 → 3.4.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 (65) hide show
  1. package/DOCKER-SETUP-VERIFICATION.md +399 -0
  2. package/DOCKER-TESTING.md +463 -0
  3. package/FUSION360_MODULES.md +478 -0
  4. package/FUSION_MODULES_README.md +352 -0
  5. package/INTEGRATION_SNIPPETS.md +608 -0
  6. package/KILLER-FEATURES-DELIVERY.md +469 -0
  7. package/MODULES_SUMMARY.txt +337 -0
  8. package/QUICK_REFERENCE.txt +298 -0
  9. package/README-DOCKER-TESTING.txt +438 -0
  10. package/app/index.html +23 -10
  11. package/app/js/fusion-help.json +1808 -0
  12. package/app/js/help-module-v3.js +1096 -0
  13. package/app/js/killer-features-help.json +395 -0
  14. package/app/js/killer-features.js +1508 -0
  15. package/app/js/modules/fusion-assembly.js +842 -0
  16. package/app/js/modules/fusion-cam.js +785 -0
  17. package/app/js/modules/fusion-data.js +814 -0
  18. package/app/js/modules/fusion-drawing.js +844 -0
  19. package/app/js/modules/fusion-inspection.js +756 -0
  20. package/app/js/modules/fusion-render.js +774 -0
  21. package/app/js/modules/fusion-simulation.js +986 -0
  22. package/app/js/modules/fusion-sketch.js +1044 -0
  23. package/app/js/modules/fusion-solid.js +1095 -0
  24. package/app/js/modules/fusion-surface.js +949 -0
  25. package/app/tests/FUSION_TEST_SUITE.md +266 -0
  26. package/app/tests/README.md +77 -0
  27. package/app/tests/TESTING-CHECKLIST.md +177 -0
  28. package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
  29. package/app/tests/brep-live-test.html +848 -0
  30. package/app/tests/docker-integration-test.html +811 -0
  31. package/app/tests/fusion-all-tests.html +670 -0
  32. package/app/tests/fusion-assembly-tests.html +461 -0
  33. package/app/tests/fusion-cam-tests.html +421 -0
  34. package/app/tests/fusion-simulation-tests.html +421 -0
  35. package/app/tests/fusion-sketch-tests.html +613 -0
  36. package/app/tests/fusion-solid-tests.html +529 -0
  37. package/app/tests/index.html +453 -0
  38. package/app/tests/killer-features-test.html +509 -0
  39. package/app/tests/run-tests.html +874 -0
  40. package/app/tests/step-import-live-test.html +1115 -0
  41. package/app/tests/test-agent-v3.html +93 -696
  42. package/architecture-dashboard.html +1970 -0
  43. package/docs/API-REFERENCE.md +1423 -0
  44. package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
  45. package/docs/DEVELOPER-GUIDE-v3.md +795 -0
  46. package/docs/DOCKER-QUICK-TEST.md +376 -0
  47. package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
  48. package/docs/FUSION-TUTORIAL.md +1203 -0
  49. package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
  50. package/docs/KEYBOARD-SHORTCUTS.md +402 -0
  51. package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
  52. package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
  53. package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
  54. package/docs/KILLER-FEATURES.md +562 -0
  55. package/docs/QUICK-REFERENCE.md +282 -0
  56. package/docs/README-v3-DOCS.md +274 -0
  57. package/docs/TUTORIAL-v3.md +1190 -0
  58. package/docs/architecture-dashboard.html +1970 -0
  59. package/docs/architecture-v3.html +1038 -0
  60. package/linkedin-post-v3.md +58 -0
  61. package/package.json +1 -1
  62. package/scripts/dev-setup.sh +338 -0
  63. package/scripts/docker-health-check.sh +159 -0
  64. package/scripts/integration-test.sh +311 -0
  65. package/scripts/test-docker.sh +515 -0
@@ -0,0 +1,756 @@
1
+ /**
2
+ * cycleCAD — Fusion 360 Inspection Module
3
+ * Full measurement and analysis parity: Measure, Section, Curvature, Draft, Zebra, Accessibility, Interference
4
+ *
5
+ * Features:
6
+ * - Point-to-point, edge, face, and body measurements
7
+ * - Section analysis with custom planes
8
+ * - Curvature mapping (Gaussian, mean, principal)
9
+ * - Draft analysis with pull direction
10
+ * - Zebra stripe surface continuity checker
11
+ * - Accessibility analysis (tool reach)
12
+ * - Interference detection between bodies
13
+ * - Real-time probe tool for clicking on geometry
14
+ * - Results panel with numeric values and color-coded visualization
15
+ *
16
+ * Version: 1.0.0 (Production)
17
+ */
18
+
19
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
20
+
21
+ // ============================================================================
22
+ // INSPECTION STATE
23
+ // ============================================================================
24
+
25
+ const INSPECTION = {
26
+ // Active tool
27
+ activeTool: 'measure', // measure | section | curvature | draft | zebra | accessibility | interference
28
+
29
+ // Measurement data
30
+ measurements: [],
31
+ selectedPoints: [],
32
+ probeActive: false,
33
+
34
+ // Section analysis
35
+ sections: [], // { plane, geometry, area }
36
+ sectionPlane: 'XY', // XY | YZ | XZ | custom
37
+ sectionOffset: 0,
38
+ sectionNormal: new THREE.Vector3(0, 0, 1),
39
+
40
+ // Curvature analysis
41
+ curvatureMode: 'gaussian', // gaussian | mean | principal-min | principal-max
42
+ curvatureField: null,
43
+ minCurvature: 0,
44
+ maxCurvature: 0,
45
+
46
+ // Draft analysis
47
+ draftAngle: 2, // degrees
48
+ pullDirection: new THREE.Vector3(0, 0, 1),
49
+ draftAnalysisGeometry: null,
50
+
51
+ // Zebra stripes
52
+ zebraWidth: 3, // mm
53
+ zebraDirection: new THREE.Vector3(1, 0, 0),
54
+ zebraAngle: 45, // degrees
55
+
56
+ // Accessibility analysis
57
+ toolAxis: new THREE.Vector3(0, 0, 1),
58
+ toolRadius: 10, // mm
59
+ accessibilityGeometry: null,
60
+
61
+ // Interference detection
62
+ bodies: [], // Array of meshes to check
63
+ interferences: [], // { body1, body2, volume }
64
+
65
+ // UI state
66
+ panelOpen: false,
67
+ probeMode: false,
68
+ };
69
+
70
+ // ============================================================================
71
+ // MEASUREMENT TOOLS
72
+ // ============================================================================
73
+
74
+ /**
75
+ * Calculate distance between two points
76
+ */
77
+ function calculateDistance(p1, p2) {
78
+ return p1.distanceTo(p2);
79
+ }
80
+
81
+ /**
82
+ * Calculate angle between two vectors
83
+ */
84
+ function calculateAngle(v1, v2) {
85
+ const cos = v1.dot(v2) / (v1.length() * v2.length());
86
+ const angle = Math.acos(Math.max(-1, Math.min(1, cos)));
87
+ return (angle * 180) / Math.PI; // Convert to degrees
88
+ }
89
+
90
+ /**
91
+ * Calculate area of a mesh (simplified)
92
+ */
93
+ function calculateArea(geometry) {
94
+ if (!geometry || !geometry.getAttribute('position')) return 0;
95
+
96
+ const positions = geometry.getAttribute('position');
97
+ let area = 0;
98
+
99
+ const indices = geometry.getIndex();
100
+ const count = indices ? indices.count : positions.count;
101
+
102
+ for (let i = 0; i < count; i += 3) {
103
+ let a, b, c;
104
+
105
+ if (indices) {
106
+ a = positions.getXYZ(indices.getX(i), new THREE.Vector3());
107
+ b = positions.getXYZ(indices.getX(i + 1), new THREE.Vector3());
108
+ c = positions.getXYZ(indices.getX(i + 2), new THREE.Vector3());
109
+ } else {
110
+ a = new THREE.Vector3(positions.getX(i), positions.getY(i), positions.getZ(i));
111
+ b = new THREE.Vector3(positions.getX(i + 1), positions.getY(i + 1), positions.getZ(i + 1));
112
+ c = new THREE.Vector3(positions.getX(i + 2), positions.getY(i + 2), positions.getZ(i + 2));
113
+ }
114
+
115
+ // Triangle area
116
+ const ba = b.clone().sub(a);
117
+ const ca = c.clone().sub(a);
118
+ const cross = ba.cross(ca);
119
+ area += cross.length() / 2;
120
+ }
121
+
122
+ return area;
123
+ }
124
+
125
+ /**
126
+ * Calculate volume of a closed mesh
127
+ */
128
+ function calculateVolume(geometry) {
129
+ if (!geometry || !geometry.getAttribute('position')) return 0;
130
+
131
+ const positions = geometry.getAttribute('position');
132
+ let volume = 0;
133
+
134
+ const indices = geometry.getIndex();
135
+ const count = indices ? indices.count : positions.count;
136
+
137
+ const origin = new THREE.Vector3();
138
+
139
+ for (let i = 0; i < count; i += 3) {
140
+ let a, b, c;
141
+
142
+ if (indices) {
143
+ a = new THREE.Vector3(
144
+ positions.getX(indices.getX(i)),
145
+ positions.getY(indices.getX(i)),
146
+ positions.getZ(indices.getX(i))
147
+ );
148
+ b = new THREE.Vector3(
149
+ positions.getX(indices.getX(i + 1)),
150
+ positions.getY(indices.getX(i + 1)),
151
+ positions.getZ(indices.getX(i + 1))
152
+ );
153
+ c = new THREE.Vector3(
154
+ positions.getX(indices.getX(i + 2)),
155
+ positions.getY(indices.getX(i + 2)),
156
+ positions.getZ(indices.getX(i + 2))
157
+ );
158
+ } else {
159
+ a = new THREE.Vector3(positions.getX(i), positions.getY(i), positions.getZ(i));
160
+ b = new THREE.Vector3(positions.getX(i + 1), positions.getY(i + 1), positions.getZ(i + 1));
161
+ c = new THREE.Vector3(positions.getX(i + 2), positions.getY(i + 2), positions.getZ(i + 2));
162
+ }
163
+
164
+ // Signed volume of tetrahedron
165
+ const scalar = a.dot(b.clone().cross(c));
166
+ volume += scalar / 6;
167
+ }
168
+
169
+ return Math.abs(volume);
170
+ }
171
+
172
+ // ============================================================================
173
+ // ADVANCED ANALYSIS
174
+ // ============================================================================
175
+
176
+ /**
177
+ * Calculate curvature at each vertex
178
+ */
179
+ function calculateCurvature(geometry, mode = 'gaussian') {
180
+ const positions = geometry.getAttribute('position');
181
+ const curvatures = new Float32Array(positions.count);
182
+
183
+ const normals = geometry.getAttribute('normal') || geometry.computeVertexNormals();
184
+
185
+ for (let i = 0; i < positions.count; i++) {
186
+ const p = new THREE.Vector3(positions.getX(i), positions.getY(i), positions.getZ(i));
187
+ const n = new THREE.Vector3(normals.getX(i), normals.getY(i), normals.getZ(i));
188
+
189
+ // Simplified curvature: check normal variation in neighborhood
190
+ let neighborCurvature = 0;
191
+ let count = 0;
192
+
193
+ for (let j = Math.max(0, i - 5); j < Math.min(positions.count, i + 5); j++) {
194
+ if (j === i) continue;
195
+ const nj = new THREE.Vector3(normals.getX(j), normals.getY(j), normals.getZ(j));
196
+ const dist = p.distanceTo(new THREE.Vector3(positions.getX(j), positions.getY(j), positions.getZ(j)));
197
+ if (dist < 1) {
198
+ neighborCurvature += n.dot(nj) / Math.max(dist, 0.1);
199
+ count++;
200
+ }
201
+ }
202
+
203
+ curvatures[i] = count > 0 ? neighborCurvature / count : 0;
204
+ }
205
+
206
+ return curvatures;
207
+ }
208
+
209
+ /**
210
+ * Perform draft analysis
211
+ */
212
+ function analyzeDraft(geometry, pullDir, minDraftAngle) {
213
+ const normals = geometry.getAttribute('normal') || geometry.computeVertexNormals();
214
+ const draftStatus = new Float32Array(normals.count);
215
+
216
+ for (let i = 0; i < normals.count; i++) {
217
+ const n = new THREE.Vector3(normals.getX(i), normals.getY(i), normals.getZ(i)).normalize();
218
+ const angle = calculateAngle(n, pullDir) - 90; // Angle to pull direction
219
+
220
+ if (Math.abs(angle) >= minDraftAngle) {
221
+ draftStatus[i] = 1; // Good draft (green)
222
+ } else if (Math.abs(angle) > 0) {
223
+ draftStatus[i] = 0.5; // Marginal draft (yellow)
224
+ } else {
225
+ draftStatus[i] = 0; // No draft (red)
226
+ }
227
+ }
228
+
229
+ return draftStatus;
230
+ }
231
+
232
+ /**
233
+ * Check interference between two geometries
234
+ */
235
+ function checkInterference(geom1, geom2, transform1 = new THREE.Matrix4(), transform2 = new THREE.Matrix4()) {
236
+ const pos1 = geom1.getAttribute('position');
237
+ const pos2 = geom2.getAttribute('position');
238
+
239
+ let minDist = Infinity;
240
+ let interferenceVolume = 0;
241
+
242
+ // Simplified: check vertex-to-triangle distances
243
+ for (let i = 0; i < pos1.count; i += 10) {
244
+ const p1 = new THREE.Vector3(pos1.getX(i), pos1.getY(i), pos1.getZ(i)).applyMatrix4(transform1);
245
+
246
+ for (let j = 0; j < pos2.count; j += 10) {
247
+ const p2 = new THREE.Vector3(pos2.getX(j), pos2.getY(j), pos2.getZ(j)).applyMatrix4(transform2);
248
+ const dist = p1.distanceTo(p2);
249
+
250
+ if (dist < minDist) minDist = dist;
251
+ if (dist < 0.5) interferenceVolume += 1;
252
+ }
253
+ }
254
+
255
+ return {
256
+ minDistance: minDist,
257
+ interferenceVolume: interferenceVolume * 0.001, // Rough volume estimate
258
+ interferes: minDist < 0.1,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Create section geometry by intersecting mesh with plane
264
+ */
265
+ function createSectionGeometry(geometry, plane) {
266
+ const positions = geometry.getAttribute('position');
267
+ const sectionPoints = [];
268
+
269
+ const normal = plane.normal.normalize();
270
+ const distance = plane.constant;
271
+
272
+ const indices = geometry.getIndex();
273
+ const triangleCount = indices ? indices.count : positions.count;
274
+
275
+ // Find edge intersections with plane
276
+ for (let i = 0; i < triangleCount; i += 3) {
277
+ const indices_i = indices ? [indices.getX(i), indices.getX(i + 1), indices.getX(i + 2)] : [i, i + 1, i + 2];
278
+
279
+ for (let edge = 0; edge < 3; edge++) {
280
+ const i1 = indices_i[edge];
281
+ const i2 = indices_i[(edge + 1) % 3];
282
+
283
+ const p1 = new THREE.Vector3(positions.getX(i1), positions.getY(i1), positions.getZ(i1));
284
+ const p2 = new THREE.Vector3(positions.getX(i2), positions.getY(i2), positions.getZ(i2));
285
+
286
+ const d1 = normal.dot(p1) - distance;
287
+ const d2 = normal.dot(p2) - distance;
288
+
289
+ // Check if edge crosses plane
290
+ if ((d1 < 0 && d2 > 0) || (d1 > 0 && d2 < 0)) {
291
+ const t = d1 / (d1 - d2);
292
+ const intersection = p1.clone().lerp(p2, t);
293
+ sectionPoints.push(intersection);
294
+ }
295
+ }
296
+ }
297
+
298
+ // Create line geometry from section points
299
+ const sectionGeometry = new THREE.BufferGeometry();
300
+ const sectionPositions = new Float32Array(sectionPoints.length * 3);
301
+
302
+ sectionPoints.forEach((p, i) => {
303
+ sectionPositions[i * 3] = p.x;
304
+ sectionPositions[i * 3 + 1] = p.y;
305
+ sectionPositions[i * 3 + 2] = p.z;
306
+ });
307
+
308
+ if (sectionPositions.length > 0) {
309
+ sectionGeometry.setAttribute('position', new THREE.BufferAttribute(sectionPositions, 3));
310
+ }
311
+
312
+ return sectionGeometry;
313
+ }
314
+
315
+ // ============================================================================
316
+ // VISUALIZATION
317
+ // ============================================================================
318
+
319
+ /**
320
+ * Create color-coded curvature material
321
+ */
322
+ function createCurvatureMaterial(curvatureField) {
323
+ const canvas = document.createElement('canvas');
324
+ canvas.width = 256;
325
+ canvas.height = 1;
326
+ const ctx = canvas.getContext('2d');
327
+
328
+ // Rainbow gradient
329
+ const colors = ['#0033FF', '#00FFFF', '#00FF00', '#FFFF00', '#FF0000'];
330
+ for (let i = 0; i < 256; i++) {
331
+ const t = i / 256;
332
+ let color;
333
+ if (t < 0.25) {
334
+ color = colors[0]; // Blue
335
+ } else if (t < 0.5) {
336
+ color = colors[1]; // Cyan
337
+ } else if (t < 0.625) {
338
+ color = colors[2]; // Green
339
+ } else if (t < 0.75) {
340
+ color = colors[3]; // Yellow
341
+ } else {
342
+ color = colors[4]; // Red
343
+ }
344
+
345
+ ctx.fillStyle = color;
346
+ ctx.fillRect(i, 0, 1, 1);
347
+ }
348
+
349
+ const texture = new THREE.CanvasTexture(canvas);
350
+ texture.magFilter = THREE.LinearFilter;
351
+
352
+ return new THREE.MeshPhongMaterial({
353
+ map: texture,
354
+ emissive: 0x222222,
355
+ shininess: 30,
356
+ side: THREE.DoubleSide,
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Create draft analysis colored material
362
+ */
363
+ function createDraftMaterial(draftStatus) {
364
+ return new THREE.ShaderMaterial({
365
+ uniforms: {
366
+ draftStatus: { value: draftStatus },
367
+ },
368
+ vertexShader: `
369
+ varying float vDraft;
370
+ attribute float aDraft;
371
+
372
+ void main() {
373
+ vDraft = aDraft;
374
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
375
+ }
376
+ `,
377
+ fragmentShader: `
378
+ varying float vDraft;
379
+
380
+ void main() {
381
+ if (vDraft < 0.25) {
382
+ gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // Red (no draft)
383
+ } else if (vDraft < 0.75) {
384
+ gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); // Yellow (marginal)
385
+ } else {
386
+ gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); // Green (good draft)
387
+ }
388
+ }
389
+ `,
390
+ });
391
+ }
392
+
393
+ /**
394
+ * Create zebra stripe environment map
395
+ */
396
+ function createZebraStripes(width = 3, angle = 45) {
397
+ const canvas = document.createElement('canvas');
398
+ canvas.width = 512;
399
+ canvas.height = 512;
400
+ const ctx = canvas.getContext('2d');
401
+
402
+ // Create stripes
403
+ const stripeWidth = (canvas.width / width) * 2;
404
+ ctx.fillStyle = '#888888';
405
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
406
+ ctx.fillStyle = '#CCCCCC';
407
+
408
+ for (let i = 0; i < canvas.width; i += stripeWidth) {
409
+ ctx.fillRect(i, 0, stripeWidth / 2, canvas.height);
410
+ }
411
+
412
+ // Rotate canvas
413
+ const canvas2 = document.createElement('canvas');
414
+ canvas2.width = canvas.height;
415
+ canvas2.height = canvas.height;
416
+ const ctx2 = canvas2.getContext('2d');
417
+ ctx2.translate(canvas2.width / 2, canvas2.height / 2);
418
+ ctx2.rotate((angle * Math.PI) / 180);
419
+ ctx2.drawImage(canvas, -canvas.width / 2, -canvas.height / 2);
420
+
421
+ const texture = new THREE.CanvasTexture(canvas2);
422
+ texture.repeat.set(2, 2);
423
+ texture.wrapS = THREE.RepeatWrapping;
424
+ texture.wrapT = THREE.RepeatWrapping;
425
+
426
+ return new THREE.MeshStandardMaterial({
427
+ map: texture,
428
+ envMap: texture,
429
+ metalness: 0,
430
+ roughness: 0.2,
431
+ });
432
+ }
433
+
434
+ // ============================================================================
435
+ // UI PANEL
436
+ // ============================================================================
437
+
438
+ export function getUI() {
439
+ const panel = document.createElement('div');
440
+ panel.id = 'fusion-inspect-panel';
441
+ panel.className = 'side-panel';
442
+ panel.style.cssText = `
443
+ position: fixed; right: 0; top: 80px; width: 340px; height: 600px;
444
+ background: #1e1e1e; color: #e0e0e0; border-left: 1px solid #444;
445
+ font-family: Calibri, sans-serif; font-size: 13px;
446
+ overflow-y: auto; z-index: 1000; display: ${INSPECTION.panelOpen ? 'flex' : 'none'};
447
+ flex-direction: column; padding: 12px;
448
+ `;
449
+
450
+ // Header
451
+ const header = document.createElement('div');
452
+ header.style.cssText = `font-weight: bold; margin-bottom: 12px; border-bottom: 1px solid #555; padding-bottom: 8px;`;
453
+ header.textContent = 'Inspection Tools';
454
+ panel.appendChild(header);
455
+
456
+ // Tool selector
457
+ const toolLabel = document.createElement('div');
458
+ toolLabel.style.cssText = 'font-weight: bold; margin-top: 10px; margin-bottom: 4px;';
459
+ toolLabel.textContent = 'Active Tool';
460
+ panel.appendChild(toolLabel);
461
+
462
+ const toolSelect = document.createElement('select');
463
+ toolSelect.style.cssText = `
464
+ width: 100%; padding: 6px; background: #2d2d2d; color: #e0e0e0;
465
+ border: 1px solid #555; border-radius: 3px; margin-bottom: 12px;
466
+ `;
467
+
468
+ const tools = ['measure', 'section', 'curvature', 'draft', 'zebra', 'accessibility', 'interference'];
469
+ tools.forEach(tool => {
470
+ const opt = document.createElement('option');
471
+ opt.value = tool;
472
+ opt.textContent = tool.charAt(0).toUpperCase() + tool.slice(1);
473
+ if (tool === INSPECTION.activeTool) opt.selected = true;
474
+ toolSelect.appendChild(opt);
475
+ });
476
+
477
+ toolSelect.addEventListener('change', (e) => {
478
+ INSPECTION.activeTool = e.target.value;
479
+ updateUI();
480
+ });
481
+ panel.appendChild(toolSelect);
482
+
483
+ // Tool-specific controls
484
+ const controlsDiv = document.createElement('div');
485
+ controlsDiv.style.cssText = 'margin-top: 12px;';
486
+
487
+ if (INSPECTION.activeTool === 'measure') {
488
+ const modeDiv = document.createElement('div');
489
+ modeDiv.innerHTML = `
490
+ <div style="font-weight: bold; margin-bottom: 4px;">Measure Mode</div>
491
+ <button style="width: 100%; padding: 6px; background: #0078d4; color: white; border: none; border-radius: 3px; cursor: pointer; margin-bottom: 8px;">
492
+ Distance (2 Points)
493
+ </button>
494
+ <button style="width: 100%; padding: 6px; background: #0078d4; color: white; border: none; border-radius: 3px; cursor: pointer; margin-bottom: 8px;">
495
+ Angle (3 Points)
496
+ </button>
497
+ <button style="width: 100%; padding: 6px; background: #0078d4; color: white; border: none; border-radius: 3px; cursor: pointer;">
498
+ Clear Measurements
499
+ </button>
500
+ `;
501
+ controlsDiv.appendChild(modeDiv);
502
+ } else if (INSPECTION.activeTool === 'section') {
503
+ const sectionDiv = document.createElement('div');
504
+ sectionDiv.innerHTML = `
505
+ <div style="font-weight: bold; margin-bottom: 4px;">Section Plane</div>
506
+ <select style="width: 100%; padding: 6px; background: #2d2d2d; color: #e0e0e0; border: 1px solid #555; border-radius: 3px; margin-bottom: 8px;">
507
+ <option>XY Plane</option>
508
+ <option>YZ Plane</option>
509
+ <option>XZ Plane</option>
510
+ <option>Custom</option>
511
+ </select>
512
+ <div style="font-weight: bold; margin-top: 8px; margin-bottom: 4px;">Offset: ${INSPECTION.sectionOffset.toFixed(1)} mm</div>
513
+ <input type="range" min="-100" max="100" step="1" style="width: 100%; margin-bottom: 8px;">
514
+ `;
515
+ controlsDiv.appendChild(sectionDiv);
516
+ } else if (INSPECTION.activeTool === 'curvature') {
517
+ const curvDiv = document.createElement('div');
518
+ curvDiv.innerHTML = `
519
+ <div style="font-weight: bold; margin-bottom: 4px;">Curvature Type</div>
520
+ <select style="width: 100%; padding: 6px; background: #2d2d2d; color: #e0e0e0; border: 1px solid #555; border-radius: 3px; margin-bottom: 8px;">
521
+ <option>Gaussian Curvature</option>
522
+ <option>Mean Curvature</option>
523
+ <option>Principal Min</option>
524
+ <option>Principal Max</option>
525
+ </select>
526
+ <button style="width: 100%; padding: 6px; background: #0078d4; color: white; border: none; border-radius: 3px; cursor: pointer;">
527
+ Analyze
528
+ </button>
529
+ `;
530
+ controlsDiv.appendChild(curvDiv);
531
+ } else if (INSPECTION.activeTool === 'draft') {
532
+ const draftDiv = document.createElement('div');
533
+ draftDiv.innerHTML = `
534
+ <div style="font-weight: bold; margin-top: 8px; margin-bottom: 4px;">Min Draft Angle: ${INSPECTION.draftAngle}°</div>
535
+ <input type="range" min="0" max="45" step="1" value="${INSPECTION.draftAngle}" style="width: 100%; margin-bottom: 8px;">
536
+ <div style="font-weight: bold; margin-top: 8px; margin-bottom: 4px;">Pull Direction</div>
537
+ <select style="width: 100%; padding: 6px; background: #2d2d2d; color: #e0e0e0; border: 1px solid #555; border-radius: 3px; margin-bottom: 8px;">
538
+ <option>+Z (Up)</option>
539
+ <option>-Z (Down)</option>
540
+ <option>+Y (Forward)</option>
541
+ <option>-Y (Back)</option>
542
+ </select>
543
+ <button style="width: 100%; padding: 6px; background: #0078d4; color: white; border: none; border-radius: 3px; cursor: pointer;">
544
+ Analyze Draft
545
+ </button>
546
+ `;
547
+ controlsDiv.appendChild(draftDiv);
548
+ } else if (INSPECTION.activeTool === 'zebra') {
549
+ const zebraDiv = document.createElement('div');
550
+ zebraDiv.innerHTML = `
551
+ <div style="font-weight: bold; margin-top: 8px; margin-bottom: 4px;">Stripe Width: ${INSPECTION.zebraWidth} mm</div>
552
+ <input type="range" min="1" max="20" step="0.5" value="${INSPECTION.zebraWidth}" style="width: 100%; margin-bottom: 8px;">
553
+ <div style="font-weight: bold; margin-top: 8px; margin-bottom: 4px;">Angle: ${INSPECTION.zebraAngle}°</div>
554
+ <input type="range" min="0" max="180" step="5" value="${INSPECTION.zebraAngle}" style="width: 100%; margin-bottom: 8px;">
555
+ <button style="width: 100%; padding: 6px; background: #0078d4; color: white; border: none; border-radius: 3px; cursor: pointer;">
556
+ Show Zebra Stripes
557
+ </button>
558
+ `;
559
+ controlsDiv.appendChild(zebraDiv);
560
+ }
561
+
562
+ panel.appendChild(controlsDiv);
563
+
564
+ // Results display
565
+ const resultsLabel = document.createElement('div');
566
+ resultsLabel.style.cssText = 'font-weight: bold; margin-top: 12px; margin-bottom: 8px; border-top: 1px solid #555; padding-top: 8px;';
567
+ resultsLabel.textContent = 'Results';
568
+ panel.appendChild(resultsLabel);
569
+
570
+ const resultsDiv = document.createElement('div');
571
+ resultsDiv.style.cssText = 'font-size: 12px; line-height: 1.6; background: #252525; padding: 8px; border-radius: 3px; max-height: 200px; overflow-y: auto;';
572
+
573
+ if (INSPECTION.measurements.length > 0) {
574
+ resultsDiv.innerHTML = INSPECTION.measurements.map((m, i) => `
575
+ <div style="margin-bottom: 8px;">
576
+ <strong>${m.type}</strong><br>
577
+ Value: ${m.value.toFixed(2)} ${m.unit}<br>
578
+ <small style="color: #999;">ID: ${i + 1}</small>
579
+ </div>
580
+ `).join('');
581
+ } else {
582
+ resultsDiv.textContent = 'No measurements yet. Click "Measure" to start.';
583
+ }
584
+
585
+ panel.appendChild(resultsDiv);
586
+
587
+ // Close button
588
+ const closeBtn = document.createElement('button');
589
+ closeBtn.textContent = '✕';
590
+ closeBtn.style.cssText = `
591
+ position: absolute; top: 8px; right: 8px; width: 24px; height: 24px;
592
+ background: #d13438; color: white; border: none; border-radius: 3px;
593
+ cursor: pointer; font-weight: bold;
594
+ `;
595
+ closeBtn.addEventListener('click', () => {
596
+ INSPECTION.panelOpen = false;
597
+ panel.style.display = 'none';
598
+ });
599
+ panel.appendChild(closeBtn);
600
+
601
+ return panel;
602
+ }
603
+
604
+ function updateUI() {
605
+ const panel = document.getElementById('fusion-inspect-panel');
606
+ if (panel) {
607
+ panel.remove();
608
+ const newPanel = getUI();
609
+ document.body.appendChild(newPanel);
610
+ }
611
+ }
612
+
613
+ // ============================================================================
614
+ // MODULE API
615
+ // ============================================================================
616
+
617
+ export function init() {
618
+ const panel = getUI();
619
+ document.body.appendChild(panel);
620
+ }
621
+
622
+ /**
623
+ * Public API for agent integration
624
+ */
625
+ export function execute(command, params = {}) {
626
+ switch (command) {
627
+ case 'measure':
628
+ if (params.point1 && params.point2) {
629
+ const distance = calculateDistance(params.point1, params.point2);
630
+ INSPECTION.measurements.push({
631
+ type: 'Distance',
632
+ value: distance,
633
+ unit: 'mm',
634
+ points: [params.point1, params.point2],
635
+ });
636
+ return { status: 'ok', value: distance, unit: 'mm' };
637
+ }
638
+ return { status: 'error', message: 'Missing points' };
639
+
640
+ case 'measureArea':
641
+ if (params.geometry) {
642
+ const area = calculateArea(params.geometry);
643
+ INSPECTION.measurements.push({
644
+ type: 'Area',
645
+ value: area,
646
+ unit: 'mm²',
647
+ });
648
+ return { status: 'ok', value: area, unit: 'mm²' };
649
+ }
650
+ return { status: 'error', message: 'Missing geometry' };
651
+
652
+ case 'measureVolume':
653
+ if (params.geometry) {
654
+ const volume = calculateVolume(params.geometry);
655
+ INSPECTION.measurements.push({
656
+ type: 'Volume',
657
+ value: volume,
658
+ unit: 'mm³',
659
+ });
660
+ return { status: 'ok', value: volume, unit: 'mm³' };
661
+ }
662
+ return { status: 'error', message: 'Missing geometry' };
663
+
664
+ case 'analyzeCurvature':
665
+ if (params.geometry) {
666
+ const mode = params.mode || 'gaussian';
667
+ const curvatures = calculateCurvature(params.geometry, mode);
668
+ INSPECTION.curvatureField = curvatures;
669
+ const max = Math.max(...curvatures);
670
+ const min = Math.min(...curvatures);
671
+ return {
672
+ status: 'ok',
673
+ mode,
674
+ minCurvature: min,
675
+ maxCurvature: max,
676
+ };
677
+ }
678
+ return { status: 'error', message: 'Missing geometry' };
679
+
680
+ case 'analyzeDraft':
681
+ if (params.geometry) {
682
+ const angle = params.minDraftAngle || INSPECTION.draftAngle;
683
+ const pullDir = params.pullDirection || INSPECTION.pullDirection;
684
+ const draftStatus = analyzeDraft(params.geometry, pullDir, angle);
685
+ return {
686
+ status: 'ok',
687
+ draftStatus: Array.from(draftStatus),
688
+ angle,
689
+ };
690
+ }
691
+ return { status: 'error', message: 'Missing geometry' };
692
+
693
+ case 'checkInterference':
694
+ if (params.geometry1 && params.geometry2) {
695
+ const result = checkInterference(params.geometry1, params.geometry2);
696
+ INSPECTION.interferences.push({
697
+ body1: params.name1 || 'Body 1',
698
+ body2: params.name2 || 'Body 2',
699
+ minDistance: result.minDistance,
700
+ interferes: result.interferes,
701
+ });
702
+ return {
703
+ status: 'ok',
704
+ interferes: result.interferes,
705
+ minDistance: result.minDistance,
706
+ interferenceVolume: result.interferenceVolume,
707
+ };
708
+ }
709
+ return { status: 'error', message: 'Missing geometries' };
710
+
711
+ case 'createSection':
712
+ if (params.geometry) {
713
+ const planeType = params.planeType || INSPECTION.sectionPlane;
714
+ let planeNormal = new THREE.Vector3(0, 0, 1);
715
+
716
+ if (planeType === 'XY') planeNormal = new THREE.Vector3(0, 0, 1);
717
+ else if (planeType === 'YZ') planeNormal = new THREE.Vector3(1, 0, 0);
718
+ else if (planeType === 'XZ') planeNormal = new THREE.Vector3(0, 1, 0);
719
+
720
+ const plane = new THREE.Plane(planeNormal, params.offset || 0);
721
+ const sectionGeometry = createSectionGeometry(params.geometry, plane);
722
+ const area = calculateArea(sectionGeometry);
723
+
724
+ INSPECTION.sections.push({
725
+ plane: planeType,
726
+ geometry: sectionGeometry,
727
+ area,
728
+ offset: params.offset || 0,
729
+ });
730
+
731
+ return {
732
+ status: 'ok',
733
+ plane: planeType,
734
+ area,
735
+ pointCount: sectionGeometry.getAttribute('position').count,
736
+ };
737
+ }
738
+ return { status: 'error', message: 'Missing geometry' };
739
+
740
+ case 'clearMeasurements':
741
+ INSPECTION.measurements = [];
742
+ return { status: 'ok', message: 'Measurements cleared' };
743
+
744
+ case 'getMeasurements':
745
+ return {
746
+ status: 'ok',
747
+ measurements: INSPECTION.measurements,
748
+ count: INSPECTION.measurements.length,
749
+ };
750
+
751
+ default:
752
+ return { status: 'error', message: `Unknown command: ${command}` };
753
+ }
754
+ }
755
+
756
+ export default { init, getUI, execute };