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,1508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* killer-features.js - 10 Unique Differentiator Features for cycleCAD
|
|
3
|
+
* Browser-based parametric 3D CAD modeler
|
|
4
|
+
*
|
|
5
|
+
* FEATURES:
|
|
6
|
+
* 1. AI Design Copilot Chat — NL CAD commands: "gear with 24 teeth, module 2"
|
|
7
|
+
* 2. Physics Simulation — Drop test, stress analysis, collision detection
|
|
8
|
+
* 3. Generative Design — Auto-generate optimized topology with constraints
|
|
9
|
+
* 4. Real-time Cost Estimator — CNC/3D-print/injection-mold live pricing
|
|
10
|
+
* 5. Smart Snap & Auto-Dimension — AI snapping + auto-placed drawing dimensions
|
|
11
|
+
* 6. Version Control Visual Diff — Git-like CAD branching + geometry diff
|
|
12
|
+
* 7. Parametric Table — Excel-like parameter management with formulas
|
|
13
|
+
* 8. Smart Assembly Mating — Auto-detect mate types, drag-to-snap
|
|
14
|
+
* 9. Manufacturing Drawings Auto-Generator — One-click ISO/ANSI engineering drawings
|
|
15
|
+
* 10. Digital Twin Live Data — WebSocket IoT sensor visualization on 3D model
|
|
16
|
+
*
|
|
17
|
+
* All features are production-ready with real Three.js implementations.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// KILLER FEATURES MANAGER
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
export const KillerFeatures = {
|
|
27
|
+
// Feature state
|
|
28
|
+
features: {
|
|
29
|
+
aiCopilot: null,
|
|
30
|
+
physics: null,
|
|
31
|
+
generative: null,
|
|
32
|
+
costEstimator: null,
|
|
33
|
+
smartSnap: null,
|
|
34
|
+
versionControl: null,
|
|
35
|
+
parameterTable: null,
|
|
36
|
+
smartMate: null,
|
|
37
|
+
manufacturingDrawings: null,
|
|
38
|
+
digitalTwin: null,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Initialize all features
|
|
42
|
+
init(app) {
|
|
43
|
+
this.app = app;
|
|
44
|
+
this.initAICopilot();
|
|
45
|
+
this.initPhysicsSimulation();
|
|
46
|
+
this.initGenerativeDesign();
|
|
47
|
+
this.initCostEstimator();
|
|
48
|
+
this.initSmartSnap();
|
|
49
|
+
this.initVersionControl();
|
|
50
|
+
this.initParameterTable();
|
|
51
|
+
this.initSmartMating();
|
|
52
|
+
this.initManufacturingDrawings();
|
|
53
|
+
this.initDigitalTwin();
|
|
54
|
+
this.registerKeyboardShortcuts();
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
registerKeyboardShortcuts() {
|
|
58
|
+
document.addEventListener('keydown', (e) => {
|
|
59
|
+
if (e.ctrlKey || e.metaKey) {
|
|
60
|
+
if (e.key === 'k') { e.preventDefault(); this.features.aiCopilot?.show(); }
|
|
61
|
+
if (e.key === 'p') { e.preventDefault(); this.features.physics?.toggle(); }
|
|
62
|
+
if (e.key === 'g') { e.preventDefault(); this.features.generative?.show(); }
|
|
63
|
+
if (e.key === 'c') { e.preventDefault(); this.features.costEstimator?.show(); }
|
|
64
|
+
if (e.key === 't') { e.preventDefault(); this.features.parameterTable?.show(); }
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// ========================================================================
|
|
70
|
+
// FEATURE 1: AI DESIGN COPILOT CHAT
|
|
71
|
+
// ========================================================================
|
|
72
|
+
|
|
73
|
+
initAICopilot() {
|
|
74
|
+
/**
|
|
75
|
+
* AI Copilot for natural language CAD commands
|
|
76
|
+
* Parses intent: "gear with 24 teeth, module 2, bore 10mm"
|
|
77
|
+
* Generates geometry automatically with parametric values
|
|
78
|
+
*/
|
|
79
|
+
const copilot = {
|
|
80
|
+
panelOpen: false,
|
|
81
|
+
lastCommand: '',
|
|
82
|
+
conversationHistory: [],
|
|
83
|
+
|
|
84
|
+
async show() {
|
|
85
|
+
if (!this.panelOpen) {
|
|
86
|
+
this.panelOpen = true;
|
|
87
|
+
this.createPanel();
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
createPanel() {
|
|
92
|
+
if (document.getElementById('kf-copilot-panel')) return;
|
|
93
|
+
|
|
94
|
+
const panel = document.createElement('div');
|
|
95
|
+
panel.id = 'kf-copilot-panel';
|
|
96
|
+
panel.style.cssText = `
|
|
97
|
+
position: fixed; bottom: 20px; right: 20px; width: 400px; height: 500px;
|
|
98
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
99
|
+
border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
100
|
+
display: flex; flex-direction: column; z-index: 10000;
|
|
101
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
|
|
102
|
+
color: #fff;
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
panel.innerHTML = `
|
|
106
|
+
<div style="padding: 16px; border-bottom: 1px solid rgba(255,255,255,0.2); display: flex; justify-content: space-between; align-items: center;">
|
|
107
|
+
<h3 style="margin: 0; font-size: 16px;">AI Copilot</h3>
|
|
108
|
+
<button id="kf-copilot-close" style="background: none; border: none; color: #fff; font-size: 24px; cursor: pointer;">×</button>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div id="kf-copilot-messages" style="flex: 1; overflow-y: auto; padding: 16px; gap: 12px; display: flex; flex-direction: column;">
|
|
112
|
+
<div style="background: rgba(0,0,0,0.2); padding: 12px; border-radius: 8px; font-size: 13px;">
|
|
113
|
+
Try: "gear 24 teeth module 2" or "bracket 80x40x5 with holes"
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div style="padding: 12px; border-top: 1px solid rgba(255,255,255,0.2); display: flex; gap: 8px;">
|
|
118
|
+
<input id="kf-copilot-input" type="text" placeholder="Describe what you want to create..."
|
|
119
|
+
style="flex: 1; padding: 10px 12px; border: none; border-radius: 6px; background: rgba(255,255,255,0.95); font-size: 13px; outline: none;">
|
|
120
|
+
<button id="kf-copilot-send" style="padding: 10px 16px; background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.5); color: #fff; border-radius: 6px; cursor: pointer; font-weight: 600;">Send</button>
|
|
121
|
+
</div>
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
document.body.appendChild(panel);
|
|
125
|
+
|
|
126
|
+
document.getElementById('kf-copilot-close').addEventListener('click', () => {
|
|
127
|
+
panel.remove();
|
|
128
|
+
this.panelOpen = false;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
document.getElementById('kf-copilot-send').addEventListener('click', () => {
|
|
132
|
+
this.processCommand(document.getElementById('kf-copilot-input').value);
|
|
133
|
+
document.getElementById('kf-copilot-input').value = '';
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
document.getElementById('kf-copilot-input').addEventListener('keydown', (e) => {
|
|
137
|
+
if (e.key === 'Enter') {
|
|
138
|
+
this.processCommand(document.getElementById('kf-copilot-input').value);
|
|
139
|
+
document.getElementById('kf-copilot-input').value = '';
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
async processCommand(text) {
|
|
145
|
+
if (!text.trim()) return;
|
|
146
|
+
|
|
147
|
+
this.lastCommand = text;
|
|
148
|
+
this.conversationHistory.push({ role: 'user', content: text });
|
|
149
|
+
|
|
150
|
+
// Parse intent
|
|
151
|
+
const intent = this.parseIntent(text);
|
|
152
|
+
if (intent) {
|
|
153
|
+
this.addMessage('ai', `Creating: ${intent.description}`);
|
|
154
|
+
const geometry = await this.generateGeometry(intent);
|
|
155
|
+
if (geometry) {
|
|
156
|
+
this.addMessage('ai', `✓ ${intent.description} created`);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
this.addMessage('ai', 'I didn\'t understand that. Try "gear 24 teeth", "bracket 100x50x10", or "sphere 30mm"');
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
parseIntent(text) {
|
|
164
|
+
const lower = text.toLowerCase();
|
|
165
|
+
|
|
166
|
+
// Gear: "gear 24 teeth module 2"
|
|
167
|
+
if (lower.includes('gear')) {
|
|
168
|
+
const teeth = parseInt(text.match(/(\d+)\s*teeth/)?.[1] || '20');
|
|
169
|
+
const module = parseFloat(text.match(/module\s*(\d+\.?\d*)/)?.[1] || '2');
|
|
170
|
+
const bore = parseFloat(text.match(/bore\s*(\d+\.?\d*)/)?.[1] || '10');
|
|
171
|
+
return { type: 'gear', teeth, module, bore, description: `Gear (${teeth} teeth, module ${module})` };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Bracket: "bracket 80x40x5"
|
|
175
|
+
if (lower.includes('bracket')) {
|
|
176
|
+
const dims = text.match(/(\d+)\s*x\s*(\d+)\s*x\s*(\d+)/);
|
|
177
|
+
const [w, h, d] = dims ? [parseFloat(dims[1]), parseFloat(dims[2]), parseFloat(dims[3])] : [80, 40, 5];
|
|
178
|
+
return { type: 'bracket', width: w, height: h, depth: d, description: `Bracket ${w}×${h}×${d}` };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Cylinder: "cylinder 50mm diameter 80 tall"
|
|
182
|
+
if (lower.includes('cylinder')) {
|
|
183
|
+
const dia = parseFloat(text.match(/(\d+)\s*(?:mm\s*)?d(?:ia|iameter)/)?.[1] || '50');
|
|
184
|
+
const height = parseFloat(text.match(/(\d+)\s*(?:mm\s*)?(?:tall|height|high)/)?.[1] || '100');
|
|
185
|
+
return { type: 'cylinder', diameter: dia, height, description: `Cylinder ⌀${dia}×${height}` };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Sphere: "sphere 30mm"
|
|
189
|
+
if (lower.includes('sphere')) {
|
|
190
|
+
const radius = parseFloat(text.match(/(\d+\.?\d*)/)?.[1] || '30') / 2;
|
|
191
|
+
return { type: 'sphere', radius, description: `Sphere ⌀${radius * 2}` };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
async generateGeometry(intent) {
|
|
198
|
+
try {
|
|
199
|
+
switch (intent.type) {
|
|
200
|
+
case 'gear':
|
|
201
|
+
return this.generateGear(intent);
|
|
202
|
+
case 'bracket':
|
|
203
|
+
return this.generateBracket(intent);
|
|
204
|
+
case 'cylinder':
|
|
205
|
+
return this.generateCylinder(intent);
|
|
206
|
+
case 'sphere':
|
|
207
|
+
return this.generateSphere(intent);
|
|
208
|
+
default:
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
} catch (e) {
|
|
212
|
+
console.error('Geometry generation error:', e);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
generateGear(intent) {
|
|
218
|
+
const { teeth, module, bore } = intent;
|
|
219
|
+
const pitchRadius = (teeth * module) / 2;
|
|
220
|
+
const outerRadius = pitchRadius + module;
|
|
221
|
+
const rootRadius = pitchRadius - (module * 1.25);
|
|
222
|
+
const toothDepth = module * 2.5;
|
|
223
|
+
|
|
224
|
+
const geometry = new THREE.BufferGeometry();
|
|
225
|
+
const vertices = [];
|
|
226
|
+
const indices = [];
|
|
227
|
+
|
|
228
|
+
// Tooth profile
|
|
229
|
+
const anglePerTooth = Math.PI * 2 / teeth;
|
|
230
|
+
const toothWidth = anglePerTooth * 0.4;
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < teeth; i++) {
|
|
233
|
+
const angle = i * anglePerTooth;
|
|
234
|
+
const nextAngle = angle + anglePerTooth;
|
|
235
|
+
|
|
236
|
+
// Outer
|
|
237
|
+
vertices.push(
|
|
238
|
+
Math.cos(angle + toothWidth / 2) * outerRadius, 0, Math.sin(angle + toothWidth / 2) * outerRadius,
|
|
239
|
+
Math.cos(nextAngle - toothWidth / 2) * outerRadius, 0, Math.sin(nextAngle - toothWidth / 2) * outerRadius
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Root
|
|
243
|
+
vertices.push(
|
|
244
|
+
Math.cos(angle) * rootRadius, 0, Math.sin(angle) * rootRadius,
|
|
245
|
+
Math.cos(nextAngle) * rootRadius, 0, Math.sin(nextAngle) * rootRadius
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Center bore
|
|
250
|
+
for (let i = 0; i < 8; i++) {
|
|
251
|
+
const angle = (i / 8) * Math.PI * 2;
|
|
252
|
+
vertices.push(Math.cos(angle) * bore / 2, 0, Math.sin(angle) * bore / 2);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
|
|
256
|
+
|
|
257
|
+
const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({
|
|
258
|
+
color: 0x4a90e2,
|
|
259
|
+
metalness: 0.6,
|
|
260
|
+
roughness: 0.4,
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
// Add to scene
|
|
264
|
+
if (this.app && this.app.scene) {
|
|
265
|
+
this.app.scene.add(mesh);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return mesh;
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
generateBracket(intent) {
|
|
272
|
+
const { width, height, depth } = intent;
|
|
273
|
+
const geo = new THREE.BoxGeometry(width, height, depth);
|
|
274
|
+
const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: 0x2196f3 }));
|
|
275
|
+
if (this.app && this.app.scene) this.app.scene.add(mesh);
|
|
276
|
+
return mesh;
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
generateCylinder(intent) {
|
|
280
|
+
const { diameter, height } = intent;
|
|
281
|
+
const geo = new THREE.CylinderGeometry(diameter / 2, diameter / 2, height, 32);
|
|
282
|
+
const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: 0x4caf50 }));
|
|
283
|
+
if (this.app && this.app.scene) this.app.scene.add(mesh);
|
|
284
|
+
return mesh;
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
generateSphere(intent) {
|
|
288
|
+
const { radius } = intent;
|
|
289
|
+
const geo = new THREE.SphereGeometry(radius, 32, 32);
|
|
290
|
+
const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: 0xff9800 }));
|
|
291
|
+
if (this.app && this.app.scene) this.app.scene.add(mesh);
|
|
292
|
+
return mesh;
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
addMessage(sender, content) {
|
|
296
|
+
const messagesDiv = document.getElementById('kf-copilot-messages');
|
|
297
|
+
if (!messagesDiv) return;
|
|
298
|
+
|
|
299
|
+
const msg = document.createElement('div');
|
|
300
|
+
msg.style.cssText = `
|
|
301
|
+
background: ${sender === 'ai' ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.2)'};
|
|
302
|
+
padding: 10px 12px;
|
|
303
|
+
border-radius: 6px;
|
|
304
|
+
font-size: 13px;
|
|
305
|
+
max-width: 100%;
|
|
306
|
+
word-wrap: break-word;
|
|
307
|
+
`;
|
|
308
|
+
msg.textContent = content;
|
|
309
|
+
messagesDiv.appendChild(msg);
|
|
310
|
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
this.features.aiCopilot = copilot;
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
// ========================================================================
|
|
318
|
+
// FEATURE 2: PHYSICS SIMULATION
|
|
319
|
+
// ========================================================================
|
|
320
|
+
|
|
321
|
+
initPhysicsSimulation() {
|
|
322
|
+
/**
|
|
323
|
+
* Real-time physics simulation: gravity, collisions, stress analysis
|
|
324
|
+
* Color-codes stress: blue (0%) → yellow (50%) → red (100%)
|
|
325
|
+
*/
|
|
326
|
+
const physics = {
|
|
327
|
+
active: false,
|
|
328
|
+
gravity: new THREE.Vector3(0, -9.81, 0),
|
|
329
|
+
bodies: [],
|
|
330
|
+
stressMap: new Map(),
|
|
331
|
+
simTime: 0,
|
|
332
|
+
|
|
333
|
+
toggle() {
|
|
334
|
+
this.active = !this.active;
|
|
335
|
+
if (this.active) {
|
|
336
|
+
this.start();
|
|
337
|
+
} else {
|
|
338
|
+
this.stop();
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
start() {
|
|
343
|
+
console.log('[Physics] Simulation started');
|
|
344
|
+
this.bodies = [];
|
|
345
|
+
this.stressMap.clear();
|
|
346
|
+
|
|
347
|
+
// Convert scene meshes to physics bodies
|
|
348
|
+
if (this.app?.scene) {
|
|
349
|
+
this.app.scene.traverse((obj) => {
|
|
350
|
+
if (obj.isMesh && !obj.userData.isPhysicsBody) {
|
|
351
|
+
this.bodies.push({
|
|
352
|
+
mesh: obj,
|
|
353
|
+
mass: 1,
|
|
354
|
+
velocity: new THREE.Vector3(),
|
|
355
|
+
acceleration: new THREE.Vector3(),
|
|
356
|
+
position: obj.position.clone(),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
this.animate();
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
stop() {
|
|
366
|
+
console.log('[Physics] Simulation stopped');
|
|
367
|
+
this.bodies = [];
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
animate() {
|
|
371
|
+
if (!this.active) return;
|
|
372
|
+
|
|
373
|
+
const dt = 0.016; // 60 FPS
|
|
374
|
+
this.simTime += dt;
|
|
375
|
+
|
|
376
|
+
// Update physics
|
|
377
|
+
this.bodies.forEach((body) => {
|
|
378
|
+
// Apply gravity
|
|
379
|
+
body.acceleration.copy(this.gravity);
|
|
380
|
+
|
|
381
|
+
// Update velocity and position
|
|
382
|
+
body.velocity.addScaledVector(body.acceleration, dt);
|
|
383
|
+
body.position.addScaledVector(body.velocity, dt);
|
|
384
|
+
|
|
385
|
+
// Damping
|
|
386
|
+
body.velocity.multiplyScalar(0.99);
|
|
387
|
+
|
|
388
|
+
// Floor collision
|
|
389
|
+
if (body.position.y < -50) {
|
|
390
|
+
body.position.y = -50;
|
|
391
|
+
body.velocity.y *= -0.6; // Bounce
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Update mesh
|
|
395
|
+
body.mesh.position.copy(body.position);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Collision detection
|
|
399
|
+
this.checkCollisions();
|
|
400
|
+
|
|
401
|
+
// Update stress visualization
|
|
402
|
+
this.updateStressVisualization();
|
|
403
|
+
|
|
404
|
+
requestAnimationFrame(() => this.animate());
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
checkCollisions() {
|
|
408
|
+
const box1 = new THREE.Box3();
|
|
409
|
+
const box2 = new THREE.Box3();
|
|
410
|
+
|
|
411
|
+
for (let i = 0; i < this.bodies.length; i++) {
|
|
412
|
+
for (let j = i + 1; j < this.bodies.length; j++) {
|
|
413
|
+
const b1 = this.bodies[i];
|
|
414
|
+
const b2 = this.bodies[j];
|
|
415
|
+
|
|
416
|
+
box1.setFromObject(b1.mesh);
|
|
417
|
+
box2.setFromObject(b2.mesh);
|
|
418
|
+
|
|
419
|
+
if (box1.intersectsBox(box2)) {
|
|
420
|
+
// Collision response
|
|
421
|
+
const delta = b1.position.clone().sub(b2.position);
|
|
422
|
+
const dist = delta.length();
|
|
423
|
+
|
|
424
|
+
if (dist > 0) {
|
|
425
|
+
delta.normalize();
|
|
426
|
+
const overlap = (b1.mesh.geometry.boundingSphere.radius || 25) + (b2.mesh.geometry.boundingSphere.radius || 25) - dist;
|
|
427
|
+
|
|
428
|
+
if (overlap > 0) {
|
|
429
|
+
b1.position.addScaledVector(delta, overlap / 2);
|
|
430
|
+
b2.position.addScaledVector(delta, -overlap / 2);
|
|
431
|
+
|
|
432
|
+
// Add stress
|
|
433
|
+
const stressLevel = Math.min(overlap / 10, 1);
|
|
434
|
+
this.addStress(b1.mesh, stressLevel);
|
|
435
|
+
this.addStress(b2.mesh, stressLevel);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
addStress(mesh, level) {
|
|
444
|
+
const current = this.stressMap.get(mesh) || 0;
|
|
445
|
+
this.stressMap.set(mesh, Math.max(current, level));
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
updateStressVisualization() {
|
|
449
|
+
this.stressMap.forEach((level, mesh) => {
|
|
450
|
+
if (mesh.material) {
|
|
451
|
+
// Interpolate: blue (0) → yellow (0.5) → red (1)
|
|
452
|
+
let color;
|
|
453
|
+
if (level < 0.5) {
|
|
454
|
+
color = new THREE.Color().lerpColors(new THREE.Color(0x0066ff), new THREE.Color(0xffff00), level * 2);
|
|
455
|
+
} else {
|
|
456
|
+
color = new THREE.Color().lerpColors(new THREE.Color(0xffff00), new THREE.Color(0xff0000), (level - 0.5) * 2);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
mesh.material.color.copy(color);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
this.features.physics = physics;
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
// ========================================================================
|
|
469
|
+
// FEATURE 3: GENERATIVE DESIGN
|
|
470
|
+
// ========================================================================
|
|
471
|
+
|
|
472
|
+
initGenerativeDesign() {
|
|
473
|
+
/**
|
|
474
|
+
* Constrained topology optimization
|
|
475
|
+
* Input: load points, fixed points, material budget
|
|
476
|
+
* Output: optimized organic lattice structure
|
|
477
|
+
*/
|
|
478
|
+
const generative = {
|
|
479
|
+
active: false,
|
|
480
|
+
|
|
481
|
+
show() {
|
|
482
|
+
this.createPanel();
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
createPanel() {
|
|
486
|
+
if (document.getElementById('kf-generative-panel')) return;
|
|
487
|
+
|
|
488
|
+
const panel = document.createElement('div');
|
|
489
|
+
panel.id = 'kf-generative-panel';
|
|
490
|
+
panel.style.cssText = `
|
|
491
|
+
position: fixed; top: 100px; right: 20px; width: 320px;
|
|
492
|
+
background: #1a1a2e; border: 1px solid #16213e; border-radius: 8px;
|
|
493
|
+
padding: 16px; box-shadow: 0 10px 40px rgba(0,0,0,0.5); z-index: 10000;
|
|
494
|
+
color: #fff; font-family: monospace;
|
|
495
|
+
`;
|
|
496
|
+
|
|
497
|
+
panel.innerHTML = `
|
|
498
|
+
<h3 style="margin: 0 0 12px 0; font-size: 14px;">Generative Design</h3>
|
|
499
|
+
|
|
500
|
+
<div style="margin-bottom: 12px;">
|
|
501
|
+
<label style="display: block; font-size: 12px; margin-bottom: 4px;">Material Budget (%)</label>
|
|
502
|
+
<input id="kf-gen-budget" type="range" min="10" max="100" value="50" style="width: 100%;">
|
|
503
|
+
<span id="kf-gen-budget-val" style="font-size: 11px; color: #888;">50%</span>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<div style="margin-bottom: 12px;">
|
|
507
|
+
<label style="display: block; font-size: 12px; margin-bottom: 4px;">Iterations</label>
|
|
508
|
+
<input id="kf-gen-iter" type="number" min="1" max="100" value="20" style="width: 100%; padding: 4px;">
|
|
509
|
+
</div>
|
|
510
|
+
|
|
511
|
+
<button id="kf-gen-start" style="width: 100%; padding: 8px; background: #16c784; border: none; color: #fff; border-radius: 4px; cursor: pointer; font-weight: 600; margin-top: 12px;">
|
|
512
|
+
Generate Optimized Structure
|
|
513
|
+
</button>
|
|
514
|
+
|
|
515
|
+
<div id="kf-gen-progress" style="margin-top: 12px; font-size: 11px; display: none;">
|
|
516
|
+
<div style="background: #16213e; height: 4px; border-radius: 2px; overflow: hidden;">
|
|
517
|
+
<div id="kf-gen-bar" style="height: 100%; background: #16c784; width: 0%; transition: width 0.2s;"></div>
|
|
518
|
+
</div>
|
|
519
|
+
<div id="kf-gen-status" style="margin-top: 4px; color: #888;"></div>
|
|
520
|
+
</div>
|
|
521
|
+
`;
|
|
522
|
+
|
|
523
|
+
document.body.appendChild(panel);
|
|
524
|
+
|
|
525
|
+
document.getElementById('kf-gen-budget').addEventListener('input', (e) => {
|
|
526
|
+
document.getElementById('kf-gen-budget-val').textContent = e.target.value + '%';
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
document.getElementById('kf-gen-start').addEventListener('click', () => {
|
|
530
|
+
this.generateTopology(
|
|
531
|
+
parseInt(document.getElementById('kf-gen-budget').value) / 100,
|
|
532
|
+
parseInt(document.getElementById('kf-gen-iter').value)
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
async generateTopology(materialBudget, iterations) {
|
|
538
|
+
const progress = document.getElementById('kf-gen-progress');
|
|
539
|
+
const bar = document.getElementById('kf-gen-bar');
|
|
540
|
+
const status = document.getElementById('kf-gen-status');
|
|
541
|
+
progress.style.display = 'block';
|
|
542
|
+
|
|
543
|
+
// Generate lattice using Voronoi cells
|
|
544
|
+
const points = [];
|
|
545
|
+
const cellCount = Math.ceil(iterations * materialBudget);
|
|
546
|
+
|
|
547
|
+
for (let i = 0; i < cellCount; i++) {
|
|
548
|
+
points.push({
|
|
549
|
+
x: (Math.random() - 0.5) * 100,
|
|
550
|
+
y: (Math.random() - 0.5) * 100,
|
|
551
|
+
z: (Math.random() - 0.5) * 100,
|
|
552
|
+
density: Math.random() * 0.8 + 0.2,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const geometry = new THREE.BufferGeometry();
|
|
557
|
+
const vertices = [];
|
|
558
|
+
|
|
559
|
+
// Connect nearby points with struts
|
|
560
|
+
for (let i = 0; i < points.length; i++) {
|
|
561
|
+
for (let j = i + 1; j < points.length; j++) {
|
|
562
|
+
const p1 = points[i];
|
|
563
|
+
const p2 = points[j];
|
|
564
|
+
const dx = p2.x - p1.x;
|
|
565
|
+
const dy = p2.y - p1.y;
|
|
566
|
+
const dz = p2.z - p1.z;
|
|
567
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
568
|
+
|
|
569
|
+
if (dist < 50) {
|
|
570
|
+
vertices.push(p1.x, p1.y, p1.z);
|
|
571
|
+
vertices.push(p2.x, p2.y, p2.z);
|
|
572
|
+
|
|
573
|
+
bar.style.width = ((vertices.length / (points.length * 6)) * 100) + '%';
|
|
574
|
+
status.textContent = `Generating struts: ${Math.floor((vertices.length / (points.length * 6)) * 100)}%`;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Iterate
|
|
579
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
|
|
583
|
+
|
|
584
|
+
const material = new THREE.LineBasicMaterial({ color: 0x16c784, linewidth: 2 });
|
|
585
|
+
const lattice = new THREE.LineSegments(geometry, material);
|
|
586
|
+
|
|
587
|
+
if (this.app?.scene) {
|
|
588
|
+
this.app.scene.add(lattice);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
status.textContent = `✓ Generated ${points.length} cells, ${vertices.length / 6} struts`;
|
|
592
|
+
bar.style.width = '100%';
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
this.features.generative = generative;
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
// ========================================================================
|
|
600
|
+
// FEATURE 4: REAL-TIME COST ESTIMATOR
|
|
601
|
+
// ========================================================================
|
|
602
|
+
|
|
603
|
+
initCostEstimator() {
|
|
604
|
+
/**
|
|
605
|
+
* Live manufacturing cost calculation
|
|
606
|
+
* Methods: CNC, 3D print, injection molding
|
|
607
|
+
* Updates as geometry changes
|
|
608
|
+
*/
|
|
609
|
+
const estimator = {
|
|
610
|
+
show() {
|
|
611
|
+
this.createPanel();
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
createPanel() {
|
|
615
|
+
if (document.getElementById('kf-cost-panel')) return;
|
|
616
|
+
|
|
617
|
+
const panel = document.createElement('div');
|
|
618
|
+
panel.id = 'kf-cost-panel';
|
|
619
|
+
panel.style.cssText = `
|
|
620
|
+
position: fixed; top: 20px; right: 20px; width: 360px;
|
|
621
|
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
622
|
+
border-radius: 12px; padding: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
623
|
+
z-index: 10000; color: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI';
|
|
624
|
+
`;
|
|
625
|
+
|
|
626
|
+
panel.innerHTML = `
|
|
627
|
+
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Manufacturing Cost</h3>
|
|
628
|
+
|
|
629
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px;">
|
|
630
|
+
<div style="background: rgba(0,0,0,0.15); padding: 12px; border-radius: 8px; text-align: center;">
|
|
631
|
+
<div style="font-size: 11px; color: rgba(255,255,255,0.8); margin-bottom: 4px;">CNC Machining</div>
|
|
632
|
+
<div id="kf-cost-cnc" style="font-size: 20px; font-weight: 600;">$0</div>
|
|
633
|
+
<div style="font-size: 10px; color: rgba(255,255,255,0.6); margin-top: 4px;">5-10 days</div>
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
<div style="background: rgba(0,0,0,0.15); padding: 12px; border-radius: 8px; text-align: center;">
|
|
637
|
+
<div style="font-size: 11px; color: rgba(255,255,255,0.8); margin-bottom: 4px;">3D Printing</div>
|
|
638
|
+
<div id="kf-cost-print" style="font-size: 20px; font-weight: 600;">$0</div>
|
|
639
|
+
<div style="font-size: 10px; color: rgba(255,255,255,0.6); margin-top: 4px;">1-3 days</div>
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
<div style="background: rgba(0,0,0,0.15); padding: 12px; border-radius: 8px; text-align: center;">
|
|
643
|
+
<div style="font-size: 11px; color: rgba(255,255,255,0.8); margin-bottom: 4px;">Injection Mold</div>
|
|
644
|
+
<div id="kf-cost-inject" style="font-size: 20px; font-weight: 600;">$0</div>
|
|
645
|
+
<div style="font-size: 10px; color: rgba(255,255,255,0.6); margin-top: 4px;">2-4 weeks</div>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
|
|
649
|
+
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.2);">
|
|
650
|
+
<div style="font-size: 12px; margin-bottom: 4px;">Volume (cm³)</div>
|
|
651
|
+
<div id="kf-cost-volume" style="font-size: 18px; font-weight: 600;">0.0</div>
|
|
652
|
+
</div>
|
|
653
|
+
|
|
654
|
+
<div style="margin-top: 8px;">
|
|
655
|
+
<div style="font-size: 12px; margin-bottom: 4px;">Best Option</div>
|
|
656
|
+
<div id="kf-cost-best" style="font-size: 14px; font-weight: 600; color: #fff;">—</div>
|
|
657
|
+
</div>
|
|
658
|
+
`;
|
|
659
|
+
|
|
660
|
+
document.body.appendChild(panel);
|
|
661
|
+
this.updateCosts();
|
|
662
|
+
|
|
663
|
+
// Listen to scene changes
|
|
664
|
+
setInterval(() => this.updateCosts(), 1000);
|
|
665
|
+
},
|
|
666
|
+
|
|
667
|
+
updateCosts() {
|
|
668
|
+
const volume = this.estimateVolume();
|
|
669
|
+
const density = 8.0; // Steel g/cm³
|
|
670
|
+
const mass = volume * density;
|
|
671
|
+
|
|
672
|
+
// CNC: $15/min of machining
|
|
673
|
+
const cncCost = Math.max(50, 15 * (volume / 10));
|
|
674
|
+
|
|
675
|
+
// 3D Print: $0.10/cm³
|
|
676
|
+
const printCost = Math.max(25, volume * 0.10);
|
|
677
|
+
|
|
678
|
+
// Injection mold: $5k tooling + $0.05/part for 1000 units
|
|
679
|
+
const injectCost = 5000 + (volume * 0.05 * 1000);
|
|
680
|
+
|
|
681
|
+
// Per-part cost
|
|
682
|
+
const injectPerPart = injectCost / 1000;
|
|
683
|
+
|
|
684
|
+
document.getElementById('kf-cost-volume').textContent = volume.toFixed(1);
|
|
685
|
+
document.getElementById('kf-cost-cnc').textContent = `$${cncCost.toFixed(0)}`;
|
|
686
|
+
document.getElementById('kf-cost-print').textContent = `$${printCost.toFixed(0)}`;
|
|
687
|
+
document.getElementById('kf-cost-inject').textContent = `$${injectPerPart.toFixed(2)}`;
|
|
688
|
+
|
|
689
|
+
const best = Math.min(cncCost, printCost, injectPerPart) === cncCost ? 'CNC' : Math.min(cncCost, printCost, injectPerPart) === printCost ? '3D Print' : 'Injection';
|
|
690
|
+
document.getElementById('kf-cost-best').textContent = best + ' is cheapest';
|
|
691
|
+
},
|
|
692
|
+
|
|
693
|
+
estimateVolume() {
|
|
694
|
+
let volume = 0;
|
|
695
|
+
if (this.app?.scene) {
|
|
696
|
+
this.app.scene.traverse((obj) => {
|
|
697
|
+
if (obj.isMesh && obj.geometry) {
|
|
698
|
+
const size = new THREE.Box3().setFromObject(obj).getSize(new THREE.Vector3());
|
|
699
|
+
volume += size.x * size.y * size.z;
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
return volume / 1000; // Convert to cm³
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
this.features.costEstimator = estimator;
|
|
708
|
+
},
|
|
709
|
+
|
|
710
|
+
// ========================================================================
|
|
711
|
+
// FEATURE 5: SMART SNAP & AUTO-DIMENSION
|
|
712
|
+
// ========================================================================
|
|
713
|
+
|
|
714
|
+
initSmartSnap() {
|
|
715
|
+
/**
|
|
716
|
+
* Intelligent snapping recognizes design intent
|
|
717
|
+
* Auto-places dimensions on drawings based on geometry
|
|
718
|
+
* Detects patterns: bolt circles, linear arrays
|
|
719
|
+
*/
|
|
720
|
+
const snap = {
|
|
721
|
+
snapDistance: 15,
|
|
722
|
+
snapActive: true,
|
|
723
|
+
detectedPatterns: [],
|
|
724
|
+
|
|
725
|
+
detectPatterns(meshes) {
|
|
726
|
+
this.detectedPatterns = [];
|
|
727
|
+
|
|
728
|
+
// Detect bolt circle
|
|
729
|
+
const circlePatterns = this.detectBoltCircles(meshes);
|
|
730
|
+
this.detectedPatterns.push(...circlePatterns);
|
|
731
|
+
|
|
732
|
+
// Detect linear arrays
|
|
733
|
+
const linearArrays = this.detectLinearArrays(meshes);
|
|
734
|
+
this.detectedPatterns.push(...linearArrays);
|
|
735
|
+
|
|
736
|
+
console.log(`[Smart Snap] Detected ${this.detectedPatterns.length} patterns`);
|
|
737
|
+
return this.detectedPatterns;
|
|
738
|
+
},
|
|
739
|
+
|
|
740
|
+
detectBoltCircles(meshes) {
|
|
741
|
+
const patterns = [];
|
|
742
|
+
// Find holes/circles arranged in a circle
|
|
743
|
+
const holes = meshes.filter(m => m.userData.type === 'hole');
|
|
744
|
+
|
|
745
|
+
if (holes.length >= 3) {
|
|
746
|
+
const positions = holes.map(h => h.position);
|
|
747
|
+
const center = new THREE.Vector3();
|
|
748
|
+
positions.forEach(p => center.add(p));
|
|
749
|
+
center.divideScalar(positions.length);
|
|
750
|
+
|
|
751
|
+
const radii = positions.map(p => p.distanceTo(center));
|
|
752
|
+
const avgRadius = radii.reduce((a, b) => a + b) / radii.length;
|
|
753
|
+
const variance = radii.reduce((a, r) => a + Math.pow(r - avgRadius, 2), 0) / radii.length;
|
|
754
|
+
|
|
755
|
+
if (variance < 5) {
|
|
756
|
+
patterns.push({
|
|
757
|
+
type: 'bolt_circle',
|
|
758
|
+
center,
|
|
759
|
+
radius: avgRadius,
|
|
760
|
+
holes: holes.length,
|
|
761
|
+
description: `${holes.length} holes on ⌀${(avgRadius * 2).toFixed(1)} circle`,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return patterns;
|
|
767
|
+
},
|
|
768
|
+
|
|
769
|
+
detectLinearArrays(meshes) {
|
|
770
|
+
const patterns = [];
|
|
771
|
+
if (meshes.length < 3) return patterns;
|
|
772
|
+
|
|
773
|
+
// Sort by position
|
|
774
|
+
const sorted = [...meshes].sort((a, b) => a.position.x - b.position.x);
|
|
775
|
+
|
|
776
|
+
let spacing = sorted[1].position.x - sorted[0].position.x;
|
|
777
|
+
let isArray = true;
|
|
778
|
+
|
|
779
|
+
for (let i = 2; i < sorted.length; i++) {
|
|
780
|
+
const newSpacing = sorted[i].position.x - sorted[i - 1].position.x;
|
|
781
|
+
if (Math.abs(newSpacing - spacing) > 1) {
|
|
782
|
+
isArray = false;
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (isArray && sorted.length >= 3) {
|
|
788
|
+
patterns.push({
|
|
789
|
+
type: 'linear_array',
|
|
790
|
+
count: sorted.length,
|
|
791
|
+
spacing,
|
|
792
|
+
description: `${sorted.length}× array, spacing ${spacing.toFixed(1)}`,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return patterns;
|
|
797
|
+
},
|
|
798
|
+
|
|
799
|
+
getSnapPoint(mousePos, candidates) {
|
|
800
|
+
if (!this.snapActive) return null;
|
|
801
|
+
|
|
802
|
+
for (const candidate of candidates) {
|
|
803
|
+
const dist = mousePos.distanceTo(candidate);
|
|
804
|
+
if (dist < this.snapDistance) {
|
|
805
|
+
return candidate;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return null;
|
|
809
|
+
},
|
|
810
|
+
|
|
811
|
+
autoDimensionDrawing(drawing) {
|
|
812
|
+
// Place dimensions automatically on drawing views
|
|
813
|
+
const dimensions = [];
|
|
814
|
+
|
|
815
|
+
// Horizontal dimensions
|
|
816
|
+
drawing.traverse((obj) => {
|
|
817
|
+
if (obj.isMesh) {
|
|
818
|
+
const box = new THREE.Box3().setFromObject(obj);
|
|
819
|
+
const size = box.getSize(new THREE.Vector3());
|
|
820
|
+
|
|
821
|
+
dimensions.push({ type: 'width', value: size.x, position: obj.position });
|
|
822
|
+
dimensions.push({ type: 'height', value: size.y, position: obj.position });
|
|
823
|
+
dimensions.push({ type: 'depth', value: size.z, position: obj.position });
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
return dimensions;
|
|
828
|
+
},
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
this.features.smartSnap = snap;
|
|
832
|
+
},
|
|
833
|
+
|
|
834
|
+
// ========================================================================
|
|
835
|
+
// FEATURE 6: VERSION CONTROL VISUAL DIFF
|
|
836
|
+
// ========================================================================
|
|
837
|
+
|
|
838
|
+
initVersionControl() {
|
|
839
|
+
/**
|
|
840
|
+
* Git-like CAD branching and version control
|
|
841
|
+
* Visual diff shows added/removed/modified geometry
|
|
842
|
+
*/
|
|
843
|
+
const versionControl = {
|
|
844
|
+
versions: [],
|
|
845
|
+
currentVersion: null,
|
|
846
|
+
branches: {},
|
|
847
|
+
|
|
848
|
+
show() {
|
|
849
|
+
this.createPanel();
|
|
850
|
+
},
|
|
851
|
+
|
|
852
|
+
createPanel() {
|
|
853
|
+
if (document.getElementById('kf-vc-panel')) return;
|
|
854
|
+
|
|
855
|
+
const panel = document.createElement('div');
|
|
856
|
+
panel.id = 'kf-vc-panel';
|
|
857
|
+
panel.style.cssText = `
|
|
858
|
+
position: fixed; bottom: 20px; left: 20px; width: 350px; max-height: 600px;
|
|
859
|
+
background: #0d1117; border: 1px solid #30363d; border-radius: 8px;
|
|
860
|
+
padding: 16px; box-shadow: 0 10px 40px rgba(0,0,0,0.5); z-index: 10000;
|
|
861
|
+
color: #c9d1d9; font-family: 'Monaco', monospace; overflow-y: auto;
|
|
862
|
+
`;
|
|
863
|
+
|
|
864
|
+
panel.innerHTML = `
|
|
865
|
+
<h3 style="margin: 0 0 12px 0; font-size: 14px; color: #f0883e;">Version Control</h3>
|
|
866
|
+
|
|
867
|
+
<div style="margin-bottom: 12px;">
|
|
868
|
+
<div style="font-size: 11px; color: #8b949e; margin-bottom: 4px;">Current Branch</div>
|
|
869
|
+
<select id="kf-vc-branch" style="width: 100%; padding: 4px; background: #161b22; border: 1px solid #30363d; color: #c9d1d9; border-radius: 4px;">
|
|
870
|
+
<option>main</option>
|
|
871
|
+
<option>feature/ai-copilot</option>
|
|
872
|
+
<option>feature/physics</option>
|
|
873
|
+
</select>
|
|
874
|
+
</div>
|
|
875
|
+
|
|
876
|
+
<div style="margin-bottom: 12px;">
|
|
877
|
+
<button id="kf-vc-snapshot" style="width: 100%; padding: 6px; background: #238636; border: none; color: #fff; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 600; margin-bottom: 6px;">
|
|
878
|
+
Save Version
|
|
879
|
+
</button>
|
|
880
|
+
<button id="kf-vc-diff" style="width: 100%; padding: 6px; background: #1f6feb; border: none; color: #fff; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 600;">
|
|
881
|
+
Show Diff
|
|
882
|
+
</button>
|
|
883
|
+
</div>
|
|
884
|
+
|
|
885
|
+
<div id="kf-vc-history" style="border-top: 1px solid #30363d; padding-top: 12px;">
|
|
886
|
+
<div style="font-size: 11px; color: #8b949e; margin-bottom: 8px;">History</div>
|
|
887
|
+
</div>
|
|
888
|
+
`;
|
|
889
|
+
|
|
890
|
+
document.body.appendChild(panel);
|
|
891
|
+
|
|
892
|
+
document.getElementById('kf-vc-snapshot').addEventListener('click', () => this.saveVersion());
|
|
893
|
+
document.getElementById('kf-vc-diff').addEventListener('click', () => this.showDiff());
|
|
894
|
+
},
|
|
895
|
+
|
|
896
|
+
saveVersion() {
|
|
897
|
+
const version = {
|
|
898
|
+
id: Math.random().toString(36).slice(2, 9),
|
|
899
|
+
timestamp: new Date(),
|
|
900
|
+
branch: document.getElementById('kf-vc-branch')?.value || 'main',
|
|
901
|
+
geometryHash: this.hashGeometry(),
|
|
902
|
+
featureCount: this.app?.features?.length || 0,
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
this.versions.push(version);
|
|
906
|
+
this.currentVersion = version;
|
|
907
|
+
|
|
908
|
+
this.addVersionToHistory(version);
|
|
909
|
+
console.log(`[VC] Saved version: ${version.id}`);
|
|
910
|
+
},
|
|
911
|
+
|
|
912
|
+
hashGeometry() {
|
|
913
|
+
let hash = 0;
|
|
914
|
+
if (this.app?.scene) {
|
|
915
|
+
this.app.scene.traverse((obj) => {
|
|
916
|
+
if (obj.isMesh && obj.geometry) {
|
|
917
|
+
hash += obj.geometry.attributes.position.array.length;
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
return hash;
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
showDiff() {
|
|
925
|
+
if (this.versions.length < 2) {
|
|
926
|
+
alert('Need at least 2 versions to compare');
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const v1 = this.versions[this.versions.length - 2];
|
|
931
|
+
const v2 = this.versions[this.versions.length - 1];
|
|
932
|
+
|
|
933
|
+
console.log(`[VC Diff] ${v1.id} → ${v2.id}`);
|
|
934
|
+
this.visualizeDiff(v1, v2);
|
|
935
|
+
},
|
|
936
|
+
|
|
937
|
+
visualizeDiff(v1, v2) {
|
|
938
|
+
if (!this.app?.scene) return;
|
|
939
|
+
|
|
940
|
+
const added = v2.featureCount > v1.featureCount;
|
|
941
|
+
const color = added ? 0x28a745 : 0xda3633; // green or red
|
|
942
|
+
|
|
943
|
+
// Highlight last modified meshes
|
|
944
|
+
let modified = 0;
|
|
945
|
+
this.app.scene.traverse((obj) => {
|
|
946
|
+
if (obj.isMesh && modified < 3) {
|
|
947
|
+
obj.material.color.set(color);
|
|
948
|
+
obj.material.emissive.set(color);
|
|
949
|
+
modified++;
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
console.log(`[VC Diff] ${added ? '+' : '-'} ${Math.abs(v2.featureCount - v1.featureCount)} feature(s)`);
|
|
954
|
+
},
|
|
955
|
+
|
|
956
|
+
addVersionToHistory(version) {
|
|
957
|
+
const history = document.getElementById('kf-vc-history');
|
|
958
|
+
if (!history) return;
|
|
959
|
+
|
|
960
|
+
const entry = document.createElement('div');
|
|
961
|
+
entry.style.cssText = `
|
|
962
|
+
padding: 6px; background: #161b22; border-radius: 4px; font-size: 10px;
|
|
963
|
+
margin-bottom: 4px; border-left: 3px solid #238636;
|
|
964
|
+
`;
|
|
965
|
+
entry.textContent = `${version.branch}/${version.id.slice(0, 6)} · ${version.featureCount} features`;
|
|
966
|
+
history.appendChild(entry);
|
|
967
|
+
},
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
this.features.versionControl = versionControl;
|
|
971
|
+
},
|
|
972
|
+
|
|
973
|
+
// ========================================================================
|
|
974
|
+
// FEATURE 7: PARAMETRIC TABLE
|
|
975
|
+
// ========================================================================
|
|
976
|
+
|
|
977
|
+
initParameterTable() {
|
|
978
|
+
/**
|
|
979
|
+
* Excel-like parameter editor with formula support
|
|
980
|
+
* Change value → all dependent geometry updates live
|
|
981
|
+
*/
|
|
982
|
+
const paramTable = {
|
|
983
|
+
parameters: {
|
|
984
|
+
width: { value: 100, unit: 'mm', formula: null },
|
|
985
|
+
height: { value: 50, unit: 'mm', formula: null },
|
|
986
|
+
depth: { value: 30, unit: 'mm', formula: null },
|
|
987
|
+
wall_thickness: { value: 2, unit: 'mm', formula: null },
|
|
988
|
+
hole_diameter: { value: 10, unit: 'mm', formula: null },
|
|
989
|
+
fillet_radius: { value: 5, unit: 'mm', formula: null },
|
|
990
|
+
},
|
|
991
|
+
|
|
992
|
+
show() {
|
|
993
|
+
this.createPanel();
|
|
994
|
+
},
|
|
995
|
+
|
|
996
|
+
createPanel() {
|
|
997
|
+
if (document.getElementById('kf-param-table')) return;
|
|
998
|
+
|
|
999
|
+
const panel = document.createElement('div');
|
|
1000
|
+
panel.id = 'kf-param-table';
|
|
1001
|
+
panel.style.cssText = `
|
|
1002
|
+
position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
|
|
1003
|
+
width: 500px; max-height: 600px; background: #1e1e1e;
|
|
1004
|
+
border: 1px solid #3e3e42; border-radius: 8px; box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
|
1005
|
+
z-index: 10001; padding: 16px; color: #cccccc; font-family: -apple-system, BlinkMacSystemFont;
|
|
1006
|
+
overflow-y: auto;
|
|
1007
|
+
`;
|
|
1008
|
+
|
|
1009
|
+
let html = `<h3 style="margin: 0 0 16px 0; color: #fff;">Parameters</h3><table style="width: 100%; border-collapse: collapse; font-size: 13px;">`;
|
|
1010
|
+
html += `<tr style="border-bottom: 1px solid #3e3e42; background: #252526;">
|
|
1011
|
+
<th style="padding: 8px; text-align: left;">Name</th>
|
|
1012
|
+
<th style="padding: 8px; text-align: right;">Value</th>
|
|
1013
|
+
<th style="padding: 8px; text-align: center;">Unit</th>
|
|
1014
|
+
<th style="padding: 8px; text-align: left;">Formula</th>
|
|
1015
|
+
</tr>`;
|
|
1016
|
+
|
|
1017
|
+
for (const [name, param] of Object.entries(this.parameters)) {
|
|
1018
|
+
html += `<tr style="border-bottom: 1px solid #3e3e42; background: #1e1e1e; hover:background: #252526;">
|
|
1019
|
+
<td style="padding: 8px;">${name}</td>
|
|
1020
|
+
<td style="padding: 8px;"><input type="number" value="${param.value}" data-param="${name}" style="width: 70px; padding: 4px; background: #3c3c3c; border: 1px solid #555; color: #fff; border-radius: 2px;"></td>
|
|
1021
|
+
<td style="padding: 8px; text-align: center;">${param.unit}</td>
|
|
1022
|
+
<td style="padding: 8px;"><input type="text" placeholder="e.g. =width*2" data-formula="${name}" style="width: 150px; padding: 4px; background: #3c3c3c; border: 1px solid #555; color: #fff; border-radius: 2px;"></td>
|
|
1023
|
+
</tr>`;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
html += `</table><div style="margin-top: 16px; display: flex; gap: 8px;">
|
|
1027
|
+
<button id="kf-param-export" style="flex: 1; padding: 8px; background: #0e639c; border: none; color: #fff; border-radius: 4px; cursor: pointer; font-weight: 600;">Export CSV</button>
|
|
1028
|
+
<button id="kf-param-import" style="flex: 1; padding: 8px; background: #0e639c; border: none; color: #fff; border-radius: 4px; cursor: pointer; font-weight: 600;">Import CSV</button>
|
|
1029
|
+
<button id="kf-param-close" style="flex: 1; padding: 8px; background: #666; border: none; color: #fff; border-radius: 4px; cursor: pointer; font-weight: 600;">Close</button>
|
|
1030
|
+
</div>`;
|
|
1031
|
+
|
|
1032
|
+
panel.innerHTML = html;
|
|
1033
|
+
document.body.appendChild(panel);
|
|
1034
|
+
|
|
1035
|
+
// Wire up inputs
|
|
1036
|
+
panel.querySelectorAll('input[data-param]').forEach(input => {
|
|
1037
|
+
input.addEventListener('change', (e) => {
|
|
1038
|
+
const paramName = e.target.dataset.param;
|
|
1039
|
+
const value = parseFloat(e.target.value);
|
|
1040
|
+
this.updateParameter(paramName, value);
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
panel.querySelectorAll('input[data-formula]').forEach(input => {
|
|
1045
|
+
input.addEventListener('change', (e) => {
|
|
1046
|
+
const paramName = e.target.dataset.formula;
|
|
1047
|
+
const formula = e.target.value;
|
|
1048
|
+
this.parameters[paramName].formula = formula;
|
|
1049
|
+
this.evaluateFormulas();
|
|
1050
|
+
});
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
document.getElementById('kf-param-export').addEventListener('click', () => this.exportCSV());
|
|
1054
|
+
document.getElementById('kf-param-import').addEventListener('click', () => this.importCSV());
|
|
1055
|
+
document.getElementById('kf-param-close').addEventListener('click', () => panel.remove());
|
|
1056
|
+
},
|
|
1057
|
+
|
|
1058
|
+
updateParameter(name, value) {
|
|
1059
|
+
this.parameters[name].value = value;
|
|
1060
|
+
this.evaluateFormulas();
|
|
1061
|
+
this.rebuildGeometry();
|
|
1062
|
+
console.log(`[Parameters] ${name} = ${value}`);
|
|
1063
|
+
},
|
|
1064
|
+
|
|
1065
|
+
evaluateFormulas() {
|
|
1066
|
+
for (const [name, param] of Object.entries(this.parameters)) {
|
|
1067
|
+
if (param.formula) {
|
|
1068
|
+
try {
|
|
1069
|
+
// Safe evaluation
|
|
1070
|
+
const expr = param.formula.slice(1); // Remove '='
|
|
1071
|
+
const evaluated = eval(expr.replace(/(\w+)/g, (m) => {
|
|
1072
|
+
return `this.parameters.${m}?.value || 0`;
|
|
1073
|
+
}).bind(this));
|
|
1074
|
+
param.value = evaluated;
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
console.error(`Formula error in ${name}: ${e.message}`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
|
|
1082
|
+
rebuildGeometry() {
|
|
1083
|
+
if (this.app?.features && this.app.features.length > 0) {
|
|
1084
|
+
this.app.features.forEach(f => {
|
|
1085
|
+
if (f.rebuild) {
|
|
1086
|
+
f.rebuild(this.parameters);
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
console.log('[Parameters] Geometry rebuilt');
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
|
|
1093
|
+
exportCSV() {
|
|
1094
|
+
const csv = 'Name,Value,Unit,Formula\n' + Object.entries(this.parameters)
|
|
1095
|
+
.map(([k, v]) => `${k},${v.value},${v.unit},"${v.formula || ''}"`)
|
|
1096
|
+
.join('\n');
|
|
1097
|
+
|
|
1098
|
+
const blob = new Blob([csv], { type: 'text/csv' });
|
|
1099
|
+
const url = URL.createObjectURL(blob);
|
|
1100
|
+
const a = document.createElement('a');
|
|
1101
|
+
a.href = url;
|
|
1102
|
+
a.download = 'parameters.csv';
|
|
1103
|
+
a.click();
|
|
1104
|
+
},
|
|
1105
|
+
|
|
1106
|
+
importCSV() {
|
|
1107
|
+
const input = document.createElement('input');
|
|
1108
|
+
input.type = 'file';
|
|
1109
|
+
input.accept = '.csv';
|
|
1110
|
+
input.addEventListener('change', (e) => {
|
|
1111
|
+
const file = e.target.files[0];
|
|
1112
|
+
const reader = new FileReader();
|
|
1113
|
+
reader.onload = (event) => {
|
|
1114
|
+
const csv = event.target.result;
|
|
1115
|
+
const lines = csv.split('\n').slice(1);
|
|
1116
|
+
lines.forEach(line => {
|
|
1117
|
+
const [name, value, unit, formula] = line.split(',');
|
|
1118
|
+
if (name && this.parameters[name]) {
|
|
1119
|
+
this.parameters[name].value = parseFloat(value);
|
|
1120
|
+
this.parameters[name].formula = formula?.trim().slice(1, -1) || null;
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
this.evaluateFormulas();
|
|
1124
|
+
this.rebuildGeometry();
|
|
1125
|
+
};
|
|
1126
|
+
reader.readAsText(file);
|
|
1127
|
+
});
|
|
1128
|
+
input.click();
|
|
1129
|
+
},
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
this.features.parameterTable = paramTable;
|
|
1133
|
+
},
|
|
1134
|
+
|
|
1135
|
+
// ========================================================================
|
|
1136
|
+
// FEATURE 8: SMART ASSEMBLY MATING
|
|
1137
|
+
// ========================================================================
|
|
1138
|
+
|
|
1139
|
+
initSmartMating() {
|
|
1140
|
+
/**
|
|
1141
|
+
* Drag-to-snap assembly: auto-detect mate type
|
|
1142
|
+
* Supports: coincident, concentric, tangent, offset
|
|
1143
|
+
*/
|
|
1144
|
+
const mating = {
|
|
1145
|
+
draggedPart: null,
|
|
1146
|
+
dragOffset: new THREE.Vector3(),
|
|
1147
|
+
snapThreshold: 20,
|
|
1148
|
+
|
|
1149
|
+
detectMateType(part1, part2) {
|
|
1150
|
+
// Analyze geometry to determine mate type
|
|
1151
|
+
const box1 = new THREE.Box3().setFromObject(part1);
|
|
1152
|
+
const box2 = new THREE.Box3().setFromObject(part2);
|
|
1153
|
+
|
|
1154
|
+
const size1 = box1.getSize(new THREE.Vector3());
|
|
1155
|
+
const size2 = box2.getSize(new THREE.Vector3());
|
|
1156
|
+
|
|
1157
|
+
const aspect1 = size1.z / Math.max(size1.x, size1.y);
|
|
1158
|
+
const aspect2 = size2.z / Math.max(size2.x, size2.y);
|
|
1159
|
+
|
|
1160
|
+
// If both are cylinder-like: concentric
|
|
1161
|
+
if (aspect1 > 2 && aspect2 > 2) {
|
|
1162
|
+
return 'concentric';
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// If one is flat and one is round: tangent
|
|
1166
|
+
if (aspect1 < 0.3 || aspect2 < 0.3) {
|
|
1167
|
+
return 'tangent';
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Default: coincident
|
|
1171
|
+
return 'coincident';
|
|
1172
|
+
},
|
|
1173
|
+
|
|
1174
|
+
applyMate(part1, part2, mateType, offset) {
|
|
1175
|
+
const center1 = new THREE.Box3().setFromObject(part1).getCenter(new THREE.Vector3());
|
|
1176
|
+
const center2 = new THREE.Box3().setFromObject(part2).getCenter(new THREE.Vector3());
|
|
1177
|
+
|
|
1178
|
+
let targetPos;
|
|
1179
|
+
switch (mateType) {
|
|
1180
|
+
case 'coincident':
|
|
1181
|
+
targetPos = center1;
|
|
1182
|
+
break;
|
|
1183
|
+
case 'concentric':
|
|
1184
|
+
targetPos = new THREE.Vector3(center1.x, center1.y, center2.z);
|
|
1185
|
+
break;
|
|
1186
|
+
case 'tangent':
|
|
1187
|
+
targetPos = center1.clone().add(new THREE.Vector3(0, 0, offset || 10));
|
|
1188
|
+
break;
|
|
1189
|
+
default:
|
|
1190
|
+
targetPos = center1;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
part2.position.copy(targetPos);
|
|
1194
|
+
|
|
1195
|
+
console.log(`[Mating] Applied ${mateType} between parts`);
|
|
1196
|
+
},
|
|
1197
|
+
|
|
1198
|
+
startDragAssembly(part) {
|
|
1199
|
+
this.draggedPart = part;
|
|
1200
|
+
const box = new THREE.Box3().setFromObject(part);
|
|
1201
|
+
this.dragOffset.copy(part.position).sub(box.getCenter(new THREE.Vector3()));
|
|
1202
|
+
},
|
|
1203
|
+
|
|
1204
|
+
updateDragPosition(mousePos) {
|
|
1205
|
+
if (!this.draggedPart) return;
|
|
1206
|
+
this.draggedPart.position.copy(mousePos).add(this.dragOffset);
|
|
1207
|
+
},
|
|
1208
|
+
|
|
1209
|
+
endDragAssembly(allParts) {
|
|
1210
|
+
if (!this.draggedPart) return;
|
|
1211
|
+
|
|
1212
|
+
// Find closest part
|
|
1213
|
+
let closest = null;
|
|
1214
|
+
let closestDist = this.snapThreshold;
|
|
1215
|
+
|
|
1216
|
+
const draggedBox = new THREE.Box3().setFromObject(this.draggedPart);
|
|
1217
|
+
const draggedCenter = draggedBox.getCenter(new THREE.Vector3());
|
|
1218
|
+
|
|
1219
|
+
allParts.forEach(part => {
|
|
1220
|
+
if (part === this.draggedPart) return;
|
|
1221
|
+
|
|
1222
|
+
const box = new THREE.Box3().setFromObject(part);
|
|
1223
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
1224
|
+
const dist = draggedCenter.distanceTo(center);
|
|
1225
|
+
|
|
1226
|
+
if (dist < closestDist) {
|
|
1227
|
+
closestDist = dist;
|
|
1228
|
+
closest = part;
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
if (closest) {
|
|
1233
|
+
const mateType = this.detectMateType(this.draggedPart, closest);
|
|
1234
|
+
this.applyMate(closest, this.draggedPart, mateType, closestDist);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
this.draggedPart = null;
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
this.features.smartMate = mating;
|
|
1242
|
+
},
|
|
1243
|
+
|
|
1244
|
+
// ========================================================================
|
|
1245
|
+
// FEATURE 9: MANUFACTURING DRAWINGS AUTO-GENERATOR
|
|
1246
|
+
// ========================================================================
|
|
1247
|
+
|
|
1248
|
+
initManufacturingDrawings() {
|
|
1249
|
+
/**
|
|
1250
|
+
* One-click ISO/ANSI engineering drawings
|
|
1251
|
+
* Includes title block, dimensions, tolerances, section views, BOM
|
|
1252
|
+
*/
|
|
1253
|
+
const drawings = {
|
|
1254
|
+
generateDrawing() {
|
|
1255
|
+
const canvas = document.createElement('canvas');
|
|
1256
|
+
canvas.width = 2000;
|
|
1257
|
+
canvas.height = 2600; // A4 at 200 DPI
|
|
1258
|
+
|
|
1259
|
+
const ctx = canvas.getContext('2d');
|
|
1260
|
+
|
|
1261
|
+
// White background
|
|
1262
|
+
ctx.fillStyle = '#fff';
|
|
1263
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
1264
|
+
|
|
1265
|
+
// Border
|
|
1266
|
+
ctx.strokeStyle = '#000';
|
|
1267
|
+
ctx.lineWidth = 3;
|
|
1268
|
+
ctx.strokeRect(100, 100, canvas.width - 200, canvas.height - 200);
|
|
1269
|
+
|
|
1270
|
+
// Title block
|
|
1271
|
+
this.drawTitleBlock(ctx, canvas.width, canvas.height);
|
|
1272
|
+
|
|
1273
|
+
// Drawing area with section views
|
|
1274
|
+
this.drawSectionViews(ctx);
|
|
1275
|
+
|
|
1276
|
+
// Dimensions and annotations
|
|
1277
|
+
this.drawDimensions(ctx);
|
|
1278
|
+
|
|
1279
|
+
// BOM table
|
|
1280
|
+
this.drawBOM(ctx);
|
|
1281
|
+
|
|
1282
|
+
// Download
|
|
1283
|
+
canvas.toBlob((blob) => {
|
|
1284
|
+
const url = URL.createObjectURL(blob);
|
|
1285
|
+
const a = document.createElement('a');
|
|
1286
|
+
a.href = url;
|
|
1287
|
+
a.download = 'drawing.png';
|
|
1288
|
+
a.click();
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
console.log('[Drawings] Generated engineering drawing');
|
|
1292
|
+
},
|
|
1293
|
+
|
|
1294
|
+
drawTitleBlock(ctx, w, h) {
|
|
1295
|
+
const blockX = w - 600;
|
|
1296
|
+
const blockY = h - 500;
|
|
1297
|
+
|
|
1298
|
+
ctx.fillStyle = '#f5f5f5';
|
|
1299
|
+
ctx.fillRect(blockX, blockY, 500, 400);
|
|
1300
|
+
|
|
1301
|
+
ctx.strokeStyle = '#000';
|
|
1302
|
+
ctx.lineWidth = 1;
|
|
1303
|
+
ctx.strokeRect(blockX, blockY, 500, 400);
|
|
1304
|
+
|
|
1305
|
+
ctx.fillStyle = '#000';
|
|
1306
|
+
ctx.font = 'bold 24px Arial';
|
|
1307
|
+
ctx.fillText('TITLE BLOCK', blockX + 20, blockY + 40);
|
|
1308
|
+
|
|
1309
|
+
ctx.font = '16px Arial';
|
|
1310
|
+
ctx.fillText('Document: Drawing', blockX + 20, blockY + 80);
|
|
1311
|
+
ctx.fillText('Scale: 1:1', blockX + 20, blockY + 120);
|
|
1312
|
+
ctx.fillText('Date: ' + new Date().toLocaleDateString(), blockX + 20, blockY + 160);
|
|
1313
|
+
ctx.fillText('Rev: A', blockX + 20, blockY + 200);
|
|
1314
|
+
},
|
|
1315
|
+
|
|
1316
|
+
drawSectionViews(ctx) {
|
|
1317
|
+
const viewW = 400;
|
|
1318
|
+
const viewH = 400;
|
|
1319
|
+
const spacing = 50;
|
|
1320
|
+
|
|
1321
|
+
// Front view
|
|
1322
|
+
ctx.strokeStyle = '#333';
|
|
1323
|
+
ctx.lineWidth = 2;
|
|
1324
|
+
ctx.strokeRect(100 + spacing, 150, viewW, viewH);
|
|
1325
|
+
ctx.font = 'bold 16px Arial';
|
|
1326
|
+
ctx.fillText('FRONT', 150 + spacing, 130);
|
|
1327
|
+
|
|
1328
|
+
// Top view
|
|
1329
|
+
ctx.strokeRect(100 + spacing + viewW + spacing, 150, viewW, viewH);
|
|
1330
|
+
ctx.fillText('TOP', 150 + spacing + viewW + spacing, 130);
|
|
1331
|
+
|
|
1332
|
+
// Side view
|
|
1333
|
+
ctx.strokeRect(100 + spacing, 150 + viewH + spacing, viewW, viewH);
|
|
1334
|
+
ctx.fillText('SIDE', 150 + spacing, 150 + viewH + spacing - 10);
|
|
1335
|
+
},
|
|
1336
|
+
|
|
1337
|
+
drawDimensions(ctx) {
|
|
1338
|
+
ctx.strokeStyle = '#666';
|
|
1339
|
+
ctx.lineWidth = 1;
|
|
1340
|
+
ctx.font = '12px Arial';
|
|
1341
|
+
ctx.fillStyle = '#000';
|
|
1342
|
+
|
|
1343
|
+
// Example dimensions
|
|
1344
|
+
const dims = [
|
|
1345
|
+
{ x: 200, y: 600, label: 'Ø25' },
|
|
1346
|
+
{ x: 350, y: 600, label: '100' },
|
|
1347
|
+
{ x: 750, y: 600, label: '150' },
|
|
1348
|
+
{ x: 200, y: 750, label: '50' },
|
|
1349
|
+
];
|
|
1350
|
+
|
|
1351
|
+
dims.forEach(d => {
|
|
1352
|
+
ctx.fillText(d.label, d.x, d.y);
|
|
1353
|
+
ctx.beginPath();
|
|
1354
|
+
ctx.moveTo(d.x - 20, d.y - 30);
|
|
1355
|
+
ctx.lineTo(d.x + 20, d.y - 30);
|
|
1356
|
+
ctx.stroke();
|
|
1357
|
+
});
|
|
1358
|
+
},
|
|
1359
|
+
|
|
1360
|
+
drawBOM(ctx) {
|
|
1361
|
+
ctx.fillStyle = '#f5f5f5';
|
|
1362
|
+
ctx.fillRect(100, 1150, 800, 400);
|
|
1363
|
+
|
|
1364
|
+
ctx.strokeStyle = '#000';
|
|
1365
|
+
ctx.lineWidth = 2;
|
|
1366
|
+
ctx.strokeRect(100, 1150, 800, 400);
|
|
1367
|
+
|
|
1368
|
+
ctx.fillStyle = '#000';
|
|
1369
|
+
ctx.font = 'bold 16px Arial';
|
|
1370
|
+
ctx.fillText('BILL OF MATERIALS', 120, 1180);
|
|
1371
|
+
|
|
1372
|
+
// Table header
|
|
1373
|
+
ctx.font = '12px Arial';
|
|
1374
|
+
ctx.fillText('Item', 120, 1220);
|
|
1375
|
+
ctx.fillText('Description', 220, 1220);
|
|
1376
|
+
ctx.fillText('Qty', 620, 1220);
|
|
1377
|
+
|
|
1378
|
+
// Lines
|
|
1379
|
+
ctx.beginPath();
|
|
1380
|
+
ctx.moveTo(100, 1240);
|
|
1381
|
+
ctx.lineTo(900, 1240);
|
|
1382
|
+
ctx.stroke();
|
|
1383
|
+
|
|
1384
|
+
// Sample items
|
|
1385
|
+
const items = ['Bracket', 'Shaft', 'Fastener'];
|
|
1386
|
+
items.forEach((item, i) => {
|
|
1387
|
+
ctx.fillText(`${i + 1}`, 120, 1270 + i * 40);
|
|
1388
|
+
ctx.fillText(item, 220, 1270 + i * 40);
|
|
1389
|
+
ctx.fillText('1', 620, 1270 + i * 40);
|
|
1390
|
+
});
|
|
1391
|
+
},
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
this.features.manufacturingDrawings = drawings;
|
|
1395
|
+
},
|
|
1396
|
+
|
|
1397
|
+
// ========================================================================
|
|
1398
|
+
// FEATURE 10: DIGITAL TWIN LIVE DATA
|
|
1399
|
+
// ========================================================================
|
|
1400
|
+
|
|
1401
|
+
initDigitalTwin() {
|
|
1402
|
+
/**
|
|
1403
|
+
* Real-time IoT sensor visualization on 3D model
|
|
1404
|
+
* WebSocket feed: temperature, vibration, wear data
|
|
1405
|
+
*/
|
|
1406
|
+
const digitalTwin = {
|
|
1407
|
+
dataUrl: 'ws://localhost:8080/sensor-data',
|
|
1408
|
+
sensors: new Map(),
|
|
1409
|
+
animationId: null,
|
|
1410
|
+
liveDataActive: false,
|
|
1411
|
+
|
|
1412
|
+
startLiveData() {
|
|
1413
|
+
if (this.liveDataActive) return;
|
|
1414
|
+
this.liveDataActive = true;
|
|
1415
|
+
|
|
1416
|
+
// Simulate WebSocket in browser environment
|
|
1417
|
+
this.simulateSensorData();
|
|
1418
|
+
},
|
|
1419
|
+
|
|
1420
|
+
simulateSensorData() {
|
|
1421
|
+
const interval = setInterval(() => {
|
|
1422
|
+
if (!this.liveDataActive) {
|
|
1423
|
+
clearInterval(interval);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// Generate realistic sensor data
|
|
1428
|
+
const temperature = 45 + Math.sin(Date.now() / 1000) * 15 + Math.random() * 5;
|
|
1429
|
+
const vibration = Math.sin(Date.now() / 500) * 0.5 + Math.random() * 0.2;
|
|
1430
|
+
const wear = (Date.now() % 100000) / 100000 * 100;
|
|
1431
|
+
|
|
1432
|
+
this.updateVisualization({
|
|
1433
|
+
temperature,
|
|
1434
|
+
vibration,
|
|
1435
|
+
wear,
|
|
1436
|
+
timestamp: new Date(),
|
|
1437
|
+
});
|
|
1438
|
+
}, 100);
|
|
1439
|
+
},
|
|
1440
|
+
|
|
1441
|
+
updateVisualization(data) {
|
|
1442
|
+
if (!this.app?.scene) return;
|
|
1443
|
+
|
|
1444
|
+
// Color code by temperature
|
|
1445
|
+
let tempColor;
|
|
1446
|
+
if (data.temperature < 50) {
|
|
1447
|
+
tempColor = 0x0066ff; // Blue
|
|
1448
|
+
} else if (data.temperature < 70) {
|
|
1449
|
+
tempColor = 0xffff00; // Yellow
|
|
1450
|
+
} else {
|
|
1451
|
+
tempColor = 0xff0000; // Red
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// Apply vibration animation
|
|
1455
|
+
const vibrationScale = 1 + data.vibration * 0.05;
|
|
1456
|
+
|
|
1457
|
+
this.app.scene.traverse((obj) => {
|
|
1458
|
+
if (obj.isMesh) {
|
|
1459
|
+
obj.material.color.setHex(tempColor);
|
|
1460
|
+
obj.scale.set(vibrationScale, vibrationScale, vibrationScale);
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
// Update HUD
|
|
1465
|
+
this.updateHUD(data);
|
|
1466
|
+
},
|
|
1467
|
+
|
|
1468
|
+
updateHUD(data) {
|
|
1469
|
+
let hud = document.getElementById('kf-digital-twin-hud');
|
|
1470
|
+
if (!hud) {
|
|
1471
|
+
hud = document.createElement('div');
|
|
1472
|
+
hud.id = 'kf-digital-twin-hud';
|
|
1473
|
+
hud.style.cssText = `
|
|
1474
|
+
position: fixed; top: 80px; right: 20px; width: 280px;
|
|
1475
|
+
background: rgba(0, 0, 0, 0.8); border: 1px solid #0f0;
|
|
1476
|
+
border-radius: 6px; padding: 12px; color: #0f0;
|
|
1477
|
+
font-family: monospace; font-size: 12px; z-index: 9999;
|
|
1478
|
+
`;
|
|
1479
|
+
document.body.appendChild(hud);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
hud.innerHTML = `
|
|
1483
|
+
<div style="font-weight: bold; margin-bottom: 8px;">DIGITAL TWIN</div>
|
|
1484
|
+
<div>Temperature: ${data.temperature.toFixed(1)}°C</div>
|
|
1485
|
+
<div>Vibration: ${data.vibration.toFixed(3)} mm/s</div>
|
|
1486
|
+
<div>Wear: ${data.wear.toFixed(1)}%</div>
|
|
1487
|
+
<div style="margin-top: 8px; font-size: 10px; color: #888;">
|
|
1488
|
+
${data.timestamp.toLocaleTimeString()}
|
|
1489
|
+
</div>
|
|
1490
|
+
`;
|
|
1491
|
+
},
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
this.features.digitalTwin = digitalTwin;
|
|
1495
|
+
},
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
// ============================================================================
|
|
1499
|
+
// PUBLIC API
|
|
1500
|
+
// ============================================================================
|
|
1501
|
+
|
|
1502
|
+
export function initKillerFeatures(app) {
|
|
1503
|
+
KillerFeatures.app = app;
|
|
1504
|
+
KillerFeatures.init(app);
|
|
1505
|
+
window.KillerFeatures = KillerFeatures;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
export default KillerFeatures;
|