cyclecad 3.2.1 → 3.5.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/CLAUDE.md +155 -1
- package/DOCKER-SETUP-VERIFICATION.md +399 -0
- package/DOCKER-TESTING.md +463 -0
- package/FUSION360_MODULES.md +478 -0
- package/FUSION_MODULES_README.md +352 -0
- package/INTEGRATION_SNIPPETS.md +608 -0
- package/KILLER-FEATURES-DELIVERY.md +469 -0
- package/MODULES_SUMMARY.txt +337 -0
- package/QUICK_REFERENCE.txt +298 -0
- package/README-DOCKER-TESTING.txt +438 -0
- package/app/index.html +23 -10
- package/app/js/fusion-help.json +1808 -0
- package/app/js/help-module-v3.js +1096 -0
- package/app/js/killer-features-help.json +395 -0
- package/app/js/killer-features.js +1508 -0
- package/app/js/modules/fusion-assembly.js +842 -0
- package/app/js/modules/fusion-cam.js +785 -0
- package/app/js/modules/fusion-data.js +814 -0
- package/app/js/modules/fusion-drawing.js +844 -0
- package/app/js/modules/fusion-inspection.js +756 -0
- package/app/js/modules/fusion-render.js +774 -0
- package/app/js/modules/fusion-simulation.js +986 -0
- package/app/js/modules/fusion-sketch.js +1044 -0
- package/app/js/modules/fusion-solid.js +1095 -0
- package/app/js/modules/fusion-surface.js +949 -0
- package/app/tests/FUSION_TEST_SUITE.md +266 -0
- package/app/tests/README.md +77 -0
- package/app/tests/TESTING-CHECKLIST.md +177 -0
- package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
- package/app/tests/brep-live-test.html +848 -0
- package/app/tests/docker-integration-test.html +811 -0
- package/app/tests/fusion-all-tests.html +670 -0
- package/app/tests/fusion-assembly-tests.html +461 -0
- package/app/tests/fusion-cam-tests.html +421 -0
- package/app/tests/fusion-simulation-tests.html +421 -0
- package/app/tests/fusion-sketch-tests.html +613 -0
- package/app/tests/fusion-solid-tests.html +529 -0
- package/app/tests/index.html +453 -0
- package/app/tests/killer-features-test.html +509 -0
- package/app/tests/run-tests.html +874 -0
- package/app/tests/step-import-live-test.html +1115 -0
- package/app/tests/test-agent-v3.html +93 -696
- package/architecture-dashboard.html +1970 -0
- package/docs/API-REFERENCE.md +1423 -0
- package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
- package/docs/DEVELOPER-GUIDE-v3.md +795 -0
- package/docs/DOCKER-QUICK-TEST.md +376 -0
- package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
- package/docs/FUSION-TUTORIAL.md +1203 -0
- package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
- package/docs/KEYBOARD-SHORTCUTS.md +402 -0
- package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
- package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
- package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
- package/docs/KILLER-FEATURES.md +562 -0
- package/docs/QUICK-REFERENCE.md +282 -0
- package/docs/README-v3-DOCS.md +274 -0
- package/docs/TUTORIAL-v3.md +1190 -0
- package/docs/architecture-dashboard.html +1970 -0
- package/docs/architecture-v3.html +1038 -0
- package/linkedin-post-v3.md +58 -0
- package/package.json +1 -1
- package/scripts/dev-setup.sh +338 -0
- package/scripts/docker-health-check.sh +159 -0
- package/scripts/integration-test.sh +311 -0
- 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 };
|