cyclecad 3.4.0 → 3.6.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/app/index.html +51 -0
- package/app/js/modules/manufacturability.js +964 -0
- package/app/js/modules/photo-to-cad.js +1344 -0
- package/app/js/modules/text-to-cad.js +1464 -0
- package/app/tests/killer-features-tests.html +1222 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TextToCAD - Natural Language to 3D Geometry with Live Preview
|
|
3
|
+
* Converts English descriptions to parametric 3D CAD models in real-time.
|
|
4
|
+
*
|
|
5
|
+
* @module TextToCAD
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - NLP parser for 50+ shape types and features
|
|
10
|
+
* - Live preview with ghost geometry as you type
|
|
11
|
+
* - Multi-step builder with state awareness
|
|
12
|
+
* - Gemini Flash API integration (with local fallback)
|
|
13
|
+
* - 3D dimension annotations
|
|
14
|
+
* - Undo/redo per step
|
|
15
|
+
* - Variant generation (3 alternatives)
|
|
16
|
+
* - Production-ready error handling
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
(function initTextToCAD() {
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
// ========== MODULE STATE ==========
|
|
23
|
+
const state = {
|
|
24
|
+
currentGeometry: null,
|
|
25
|
+
previewGeometry: null,
|
|
26
|
+
steps: [],
|
|
27
|
+
currentStepIndex: -1,
|
|
28
|
+
scene: null,
|
|
29
|
+
renderer: null,
|
|
30
|
+
parseDebounceTimer: null,
|
|
31
|
+
lastParsedInput: '',
|
|
32
|
+
confidence: 1.0,
|
|
33
|
+
variants: [],
|
|
34
|
+
isGenerating: false,
|
|
35
|
+
lastAction: null
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ========== SHAPE VOCABULARY & PATTERNS ==========
|
|
39
|
+
const SHAPE_VOCAB = {
|
|
40
|
+
// Basic primitives
|
|
41
|
+
cylinder: { alias: ['cyl', 'tube', 'pipe'], params: ['diameter', 'radius', 'height', 'tall'] },
|
|
42
|
+
box: { alias: ['cube', 'block', 'rectangular'], params: ['width', 'height', 'depth', 'length'] },
|
|
43
|
+
sphere: { alias: ['ball', 'round', 'spherical'], params: ['diameter', 'radius'] },
|
|
44
|
+
cone: { alias: ['taper', 'conical'], params: ['diameter', 'radius', 'height', 'angle'] },
|
|
45
|
+
torus: { alias: ['donut', 'ring', 'washer'], params: ['major-radius', 'minor-radius'] },
|
|
46
|
+
|
|
47
|
+
// Mechanical parts
|
|
48
|
+
plate: { alias: ['flat', 'sheet', 'pad'], params: ['width', 'height', 'thickness'] },
|
|
49
|
+
bracket: { alias: ['angle-bracket', 'support'], params: ['width', 'height', 'thickness'] },
|
|
50
|
+
gear: { alias: ['cog', 'sprocket'], params: ['teeth', 'module', 'diameter'] },
|
|
51
|
+
flange: { alias: ['rim', 'collar'], params: ['outer-diameter', 'inner-diameter', 'thickness'] },
|
|
52
|
+
boss: { alias: ['pad', 'raised'], params: ['diameter', 'height'] },
|
|
53
|
+
rib: { alias: ['web', 'reinforcement'], params: ['width', 'height', 'thickness'] },
|
|
54
|
+
shaft: { alias: ['axle', 'spindle'], params: ['diameter', 'length'] },
|
|
55
|
+
bushing: { alias: ['bearing-insert'], params: ['outer-diameter', 'inner-diameter', 'length'] },
|
|
56
|
+
spacer: { alias: ['shim', 'distance-ring'], params: ['diameter', 'thickness'] },
|
|
57
|
+
|
|
58
|
+
// Fasteners
|
|
59
|
+
bolt: { alias: ['screw', 'cap-screw'], params: ['diameter', 'length'] },
|
|
60
|
+
nut: { alias: ['hex-nut'], params: ['width', 'height'] },
|
|
61
|
+
|
|
62
|
+
// Complex shapes
|
|
63
|
+
housing: { alias: ['enclosure', 'case', 'container'], params: ['width', 'height', 'depth'] },
|
|
64
|
+
keyway: { alias: ['key-slot'], params: ['width', 'depth', 'length'] }
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const FEATURE_PATTERNS = {
|
|
68
|
+
hole: /(\d+(?:\.\d+)?)\s*mm\s+(?:diameter|dia|ø)\s+(?:hole|through|blind)/gi,
|
|
69
|
+
counterbore: /counterbore|cbore|counter.?bore/gi,
|
|
70
|
+
countersink: /countersink|csk|counter.?sink/gi,
|
|
71
|
+
thread: /thread|m\d+|metric/gi,
|
|
72
|
+
fillet: /fillet|radius|round(?:ed)?/gi,
|
|
73
|
+
chamfer: /chamfer|bevel|45.?degree/gi,
|
|
74
|
+
pattern: /pattern|array|circular|rectangular|repeat/gi,
|
|
75
|
+
slot: /slot|keyway|groove|channel/gi
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const UNIT_PATTERNS = {
|
|
79
|
+
mm: /(\d+(?:\.\d+)?)\s*(?:mm|millimeters?)/gi,
|
|
80
|
+
cm: /(\d+(?:\.\d+)?)\s*(?:cm|centimeters?)/gi,
|
|
81
|
+
inch: /(\d+(?:\.\d+)?)\s*(?:"|in|inches?)/gi,
|
|
82
|
+
m: /(\d+(?:\.\d+)?)\s*(?:m|meters?)\s+(?!m)/gi
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const RELATIONSHIP_PATTERNS = {
|
|
86
|
+
'on-top': /on\s+(?:top|above)/gi,
|
|
87
|
+
'centered': /centered?|center|middle/gi,
|
|
88
|
+
'offset': /offset\s+by|separated\s+by/gi,
|
|
89
|
+
'through-center': /through\s+(?:the\s+)?center|axially/gi,
|
|
90
|
+
'pcd': /(\d+(?:\.\d+)?)\s*mm\s+pcd|pitch\s+circle/gi
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ========== NLP PARSER (~400 lines) ==========
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse natural language description into structured CAD commands
|
|
97
|
+
* @param {string} input - English description
|
|
98
|
+
* @returns {Object} Structured geometry specification
|
|
99
|
+
*/
|
|
100
|
+
function parseDescription(input) {
|
|
101
|
+
if (!input || input.trim().length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const lower = input.toLowerCase();
|
|
106
|
+
const spec = {
|
|
107
|
+
intent: detectIntent(input),
|
|
108
|
+
primaryShape: detectShape(input),
|
|
109
|
+
dimensions: extractDimensions(input),
|
|
110
|
+
features: extractFeatures(input),
|
|
111
|
+
relationships: extractRelationships(input),
|
|
112
|
+
parameters: {},
|
|
113
|
+
confidence: 0.9
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Build parameters from detected shape
|
|
117
|
+
if (spec.primaryShape) {
|
|
118
|
+
spec.parameters = buildShapeParameters(spec);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Calculate confidence score
|
|
122
|
+
spec.confidence = calculateConfidence(input, spec);
|
|
123
|
+
|
|
124
|
+
state.lastParsedInput = input;
|
|
125
|
+
state.confidence = spec.confidence;
|
|
126
|
+
|
|
127
|
+
return spec;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Detect user intent from input
|
|
132
|
+
* @param {string} input
|
|
133
|
+
* @returns {string} Intent type
|
|
134
|
+
*/
|
|
135
|
+
function detectIntent(input) {
|
|
136
|
+
const lower = input.toLowerCase();
|
|
137
|
+
if (/^(create|make|draw|build|generate)/.test(lower)) return 'create';
|
|
138
|
+
if (/add|with|plus/.test(lower)) return 'add';
|
|
139
|
+
if (/(fillet|chamfer|pattern|shell|subtract|cut)/.test(lower)) return 'modify';
|
|
140
|
+
if (/combine|merge|join|union/.test(lower)) return 'combine';
|
|
141
|
+
if /(array|repeat|pattern)/.test(lower)) return 'pattern';
|
|
142
|
+
if (/export|save|output/.test(lower)) return 'export';
|
|
143
|
+
return 'create';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Detect primary shape from natural language
|
|
148
|
+
* @param {string} input
|
|
149
|
+
* @returns {string|null} Shape type
|
|
150
|
+
*/
|
|
151
|
+
function detectShape(input) {
|
|
152
|
+
const lower = input.toLowerCase();
|
|
153
|
+
|
|
154
|
+
for (const [shape, vocab] of Object.entries(SHAPE_VOCAB)) {
|
|
155
|
+
const regex = new RegExp(`\\b(${shape}|${vocab.alias.join('|')})\\b`, 'i');
|
|
156
|
+
if (regex.test(lower)) {
|
|
157
|
+
return shape;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Fallback heuristics
|
|
162
|
+
if (/round|circular|cylinder/.test(lower)) return 'cylinder';
|
|
163
|
+
if (/rectangular|square|box/.test(lower)) return 'box';
|
|
164
|
+
if (/sphere|ball/.test(lower)) return 'sphere';
|
|
165
|
+
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract numerical dimensions with unit conversion
|
|
171
|
+
* @param {string} input
|
|
172
|
+
* @returns {Object} Dimensions in mm
|
|
173
|
+
*/
|
|
174
|
+
function extractDimensions(input) {
|
|
175
|
+
const dimensions = {};
|
|
176
|
+
const units = {};
|
|
177
|
+
|
|
178
|
+
// Extract all numbers with units
|
|
179
|
+
for (const [unit, pattern] of Object.entries(UNIT_PATTERNS)) {
|
|
180
|
+
let match;
|
|
181
|
+
while ((match = pattern.exec(input)) !== null) {
|
|
182
|
+
const value = parseFloat(match[1]);
|
|
183
|
+
units[match[0]] = convertToMM(value, unit);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Label dimensions by position/context
|
|
188
|
+
const numberPattern = /(\d+(?:\.\d+)?)/g;
|
|
189
|
+
let matches = [];
|
|
190
|
+
let m;
|
|
191
|
+
while ((m = numberPattern.exec(input)) !== null) {
|
|
192
|
+
matches.push({ value: parseFloat(m[1]), index: m.index });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Assign to common parameters
|
|
196
|
+
if (matches.length >= 1) dimensions.diameter = dimensions.radius = matches[0].value;
|
|
197
|
+
if (matches.length >= 2) dimensions.height = matches[1].value;
|
|
198
|
+
if (matches.length >= 3) dimensions.width = matches[2].value;
|
|
199
|
+
if (matches.length >= 4) dimensions.depth = matches[3].value;
|
|
200
|
+
|
|
201
|
+
// Check for explicit labels
|
|
202
|
+
if (/(\d+(?:\.\d+)?)\s*mm\s+(?:diameter|dia|ø)/.test(input)) {
|
|
203
|
+
const m = /(\d+(?:\.\d+)?)\s*mm\s+(?:diameter|dia|ø)/.exec(input);
|
|
204
|
+
dimensions.diameter = parseFloat(m[1]);
|
|
205
|
+
dimensions.radius = dimensions.diameter / 2;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (/(\d+(?:\.\d+)?)\s*mm\s+(?:tall|height|high)/.test(input)) {
|
|
209
|
+
const m = /(\d+(?:\.\d+)?)\s*mm\s+(?:tall|height|high)/.exec(input);
|
|
210
|
+
dimensions.height = parseFloat(m[1]);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (/(\d+(?:\.\d+)?)\s*teeth/.test(input)) {
|
|
214
|
+
const m = /(\d+(?:\.\d+)?)\s*teeth/.exec(input);
|
|
215
|
+
dimensions.teeth = parseInt(m[1]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (/module\s+(\d+(?:\.\d+)?)/.test(input)) {
|
|
219
|
+
const m = /module\s+(\d+(?:\.\d+)?)/.exec(input);
|
|
220
|
+
dimensions.module = parseFloat(m[1]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (/(\d+)\s*mm\s+pcd/.test(input)) {
|
|
224
|
+
const m = /(\d+)\s*mm\s+pcd/.exec(input);
|
|
225
|
+
dimensions.pcd = parseInt(m[1]);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return dimensions;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Extract features from input
|
|
233
|
+
* @param {string} input
|
|
234
|
+
* @returns {Array} Feature specifications
|
|
235
|
+
*/
|
|
236
|
+
function extractFeatures(input) {
|
|
237
|
+
const features = [];
|
|
238
|
+
const lower = input.toLowerCase();
|
|
239
|
+
|
|
240
|
+
// Holes
|
|
241
|
+
const holeMatches = input.match(/(\d+(?:\.\d+)?)\s*mm\s+(?:diameter|dia)?\s*(?:hole|through|blind)?/gi);
|
|
242
|
+
if (holeMatches) {
|
|
243
|
+
holeMatches.forEach((match, idx) => {
|
|
244
|
+
const diameter = parseFloat(match);
|
|
245
|
+
let type = 'through';
|
|
246
|
+
if (/blind/.test(lower)) type = 'blind';
|
|
247
|
+
if (/counterbore|cbore/.test(lower)) type = 'counterbore';
|
|
248
|
+
if (/countersink|csk/.test(lower)) type = 'countersink';
|
|
249
|
+
|
|
250
|
+
features.push({
|
|
251
|
+
type: 'hole',
|
|
252
|
+
diameter,
|
|
253
|
+
kind: type,
|
|
254
|
+
position: idx === 0 ? 'center' : `position-${idx}`
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Fillets
|
|
260
|
+
if (/fillet/.test(lower)) {
|
|
261
|
+
const m = /fillet\s+(\d+(?:\.\d+)?)\s*mm/.exec(input);
|
|
262
|
+
features.push({
|
|
263
|
+
type: 'fillet',
|
|
264
|
+
radius: m ? parseFloat(m[1]) : 2
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Chamfers
|
|
269
|
+
if (/chamfer/.test(lower)) {
|
|
270
|
+
const m = /chamfer\s+(\d+(?:\.\d+)?)\s*(?:x|by)?\s*(\d+(?:\.\d+)?)?/.exec(input);
|
|
271
|
+
features.push({
|
|
272
|
+
type: 'chamfer',
|
|
273
|
+
distance: m ? parseFloat(m[1]) : 1,
|
|
274
|
+
angle: m && m[2] ? parseFloat(m[2]) : 45
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Patterns
|
|
279
|
+
if (/pattern|array/.test(lower)) {
|
|
280
|
+
const circMatches = /(\d+)\s*(?:x|around)\s+center|circular\s+(?:array|pattern)?\s+(\d+)/.exec(input);
|
|
281
|
+
const rectMatches = /(\d+)\s*x\s+(\d+)\s+(?:array|pattern|grid)/.exec(input);
|
|
282
|
+
|
|
283
|
+
if (circMatches) {
|
|
284
|
+
features.push({
|
|
285
|
+
type: 'pattern',
|
|
286
|
+
kind: 'circular',
|
|
287
|
+
count: parseInt(circMatches[1] || circMatches[2])
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (rectMatches) {
|
|
291
|
+
features.push({
|
|
292
|
+
type: 'pattern',
|
|
293
|
+
kind: 'rectangular',
|
|
294
|
+
countX: parseInt(rectMatches[1]),
|
|
295
|
+
countY: parseInt(rectMatches[2])
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Threads
|
|
301
|
+
if (/thread|m\d+/.test(lower)) {
|
|
302
|
+
const m = /(m\d+|metric|thread)/.exec(lower);
|
|
303
|
+
features.push({
|
|
304
|
+
type: 'thread',
|
|
305
|
+
kind: m ? 'metric' : 'custom'
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return features;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Extract spatial relationships
|
|
314
|
+
* @param {string} input
|
|
315
|
+
* @returns {Object} Relationship map
|
|
316
|
+
*/
|
|
317
|
+
function extractRelationships(input) {
|
|
318
|
+
const relationships = {};
|
|
319
|
+
const lower = input.toLowerCase();
|
|
320
|
+
|
|
321
|
+
for (const [rel, pattern] of Object.entries(RELATIONSHIP_PATTERNS)) {
|
|
322
|
+
if (pattern.test(lower)) {
|
|
323
|
+
relationships[rel] = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// PCD detection
|
|
328
|
+
const pcdMatch = /(\d+(?:\.\d+)?)\s*mm\s+pcd/.exec(input);
|
|
329
|
+
if (pcdMatch) {
|
|
330
|
+
relationships['pcd'] = parseFloat(pcdMatch[1]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return relationships;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Build THREE.js geometry from parsed spec
|
|
338
|
+
* @param {Object} spec - Parsed specification
|
|
339
|
+
* @returns {Object} Parameters for geometry creation
|
|
340
|
+
*/
|
|
341
|
+
function buildShapeParameters(spec) {
|
|
342
|
+
const params = { ...spec.dimensions };
|
|
343
|
+
const shape = spec.primaryShape;
|
|
344
|
+
|
|
345
|
+
if (!shape) return params;
|
|
346
|
+
|
|
347
|
+
// Set defaults based on shape
|
|
348
|
+
switch (shape) {
|
|
349
|
+
case 'cylinder':
|
|
350
|
+
params.radius = params.radius || params.diameter / 2 || 25;
|
|
351
|
+
params.height = params.height || 50;
|
|
352
|
+
params.radialSegments = 32;
|
|
353
|
+
break;
|
|
354
|
+
case 'box':
|
|
355
|
+
params.width = params.width || 50;
|
|
356
|
+
params.height = params.height || 50;
|
|
357
|
+
params.depth = params.depth || 50;
|
|
358
|
+
break;
|
|
359
|
+
case 'sphere':
|
|
360
|
+
params.radius = params.radius || params.diameter / 2 || 25;
|
|
361
|
+
params.widthSegments = 32;
|
|
362
|
+
params.heightSegments = 32;
|
|
363
|
+
break;
|
|
364
|
+
case 'cone':
|
|
365
|
+
params.radius = params.radius || params.diameter / 2 || 25;
|
|
366
|
+
params.height = params.height || 50;
|
|
367
|
+
params.radialSegments = 32;
|
|
368
|
+
break;
|
|
369
|
+
case 'gear':
|
|
370
|
+
params.teeth = params.teeth || 24;
|
|
371
|
+
params.module = params.module || 2;
|
|
372
|
+
params.pressure_angle = 20;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return params;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Calculate parse confidence score (0-1)
|
|
381
|
+
* @param {string} input
|
|
382
|
+
* @param {Object} spec
|
|
383
|
+
* @returns {number} Confidence score
|
|
384
|
+
*/
|
|
385
|
+
function calculateConfidence(input, spec) {
|
|
386
|
+
let score = 0.5;
|
|
387
|
+
|
|
388
|
+
if (spec.primaryShape) score += 0.2;
|
|
389
|
+
if (Object.keys(spec.dimensions).length > 0) score += 0.15;
|
|
390
|
+
if (spec.features.length > 0) score += 0.1;
|
|
391
|
+
if (spec.relationships && Object.keys(spec.relationships).length > 0) score += 0.05;
|
|
392
|
+
if (/^(create|make|draw|build)/.test(input.toLowerCase())) score += 0.1;
|
|
393
|
+
|
|
394
|
+
// Reduce confidence if input is ambiguous or short
|
|
395
|
+
if (input.length < 10) score -= 0.1;
|
|
396
|
+
if (/[?!]$/.test(input)) score -= 0.05;
|
|
397
|
+
|
|
398
|
+
return Math.max(0, Math.min(1, score));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Convert value to millimeters
|
|
403
|
+
* @param {number} value
|
|
404
|
+
* @param {string} unit
|
|
405
|
+
* @returns {number} Value in mm
|
|
406
|
+
*/
|
|
407
|
+
function convertToMM(value, unit) {
|
|
408
|
+
switch (unit) {
|
|
409
|
+
case 'mm': return value;
|
|
410
|
+
case 'cm': return value * 10;
|
|
411
|
+
case 'inch': return value * 25.4;
|
|
412
|
+
case 'm': return value * 1000;
|
|
413
|
+
default: return value;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ========== GEOMETRY GENERATION (~300 lines) ==========
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Generate THREE.js geometry from parsed specification
|
|
421
|
+
* @param {Object} spec - Parsed CAD specification
|
|
422
|
+
* @returns {THREE.Group} Composite 3D geometry
|
|
423
|
+
*/
|
|
424
|
+
function generateGeometry(spec) {
|
|
425
|
+
if (!spec || !spec.primaryShape) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const group = new THREE.Group();
|
|
430
|
+
const shape = spec.primaryShape;
|
|
431
|
+
const params = spec.parameters;
|
|
432
|
+
|
|
433
|
+
let geometry;
|
|
434
|
+
|
|
435
|
+
switch (shape) {
|
|
436
|
+
case 'cylinder':
|
|
437
|
+
geometry = new THREE.CylinderGeometry(
|
|
438
|
+
params.radius,
|
|
439
|
+
params.radius,
|
|
440
|
+
params.height,
|
|
441
|
+
params.radialSegments || 32
|
|
442
|
+
);
|
|
443
|
+
break;
|
|
444
|
+
|
|
445
|
+
case 'box':
|
|
446
|
+
geometry = new THREE.BoxGeometry(
|
|
447
|
+
params.width,
|
|
448
|
+
params.height,
|
|
449
|
+
params.depth
|
|
450
|
+
);
|
|
451
|
+
break;
|
|
452
|
+
|
|
453
|
+
case 'sphere':
|
|
454
|
+
geometry = new THREE.SphereGeometry(
|
|
455
|
+
params.radius,
|
|
456
|
+
params.widthSegments || 32,
|
|
457
|
+
params.heightSegments || 32
|
|
458
|
+
);
|
|
459
|
+
break;
|
|
460
|
+
|
|
461
|
+
case 'cone':
|
|
462
|
+
geometry = new THREE.ConeGeometry(
|
|
463
|
+
params.radius,
|
|
464
|
+
params.height,
|
|
465
|
+
params.radialSegments || 32
|
|
466
|
+
);
|
|
467
|
+
break;
|
|
468
|
+
|
|
469
|
+
case 'torus':
|
|
470
|
+
geometry = new THREE.TorusGeometry(
|
|
471
|
+
params['major-radius'] || 40,
|
|
472
|
+
params['minor-radius'] || 15,
|
|
473
|
+
32,
|
|
474
|
+
100
|
|
475
|
+
);
|
|
476
|
+
break;
|
|
477
|
+
|
|
478
|
+
case 'gear':
|
|
479
|
+
geometry = createGearGeometry(params);
|
|
480
|
+
break;
|
|
481
|
+
|
|
482
|
+
case 'plate':
|
|
483
|
+
geometry = new THREE.BoxGeometry(
|
|
484
|
+
params.width || 100,
|
|
485
|
+
params.thickness || 5,
|
|
486
|
+
params.height || 100
|
|
487
|
+
);
|
|
488
|
+
break;
|
|
489
|
+
|
|
490
|
+
case 'bracket':
|
|
491
|
+
geometry = createBracketGeometry(params);
|
|
492
|
+
break;
|
|
493
|
+
|
|
494
|
+
case 'flange':
|
|
495
|
+
geometry = createFlangeGeometry(params);
|
|
496
|
+
break;
|
|
497
|
+
|
|
498
|
+
case 'housing':
|
|
499
|
+
geometry = createHousingGeometry(params);
|
|
500
|
+
break;
|
|
501
|
+
|
|
502
|
+
default:
|
|
503
|
+
geometry = new THREE.CylinderGeometry(25, 25, 50, 32);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (geometry) {
|
|
507
|
+
const material = new THREE.MeshPhongMaterial({
|
|
508
|
+
color: 0x0284C7,
|
|
509
|
+
emissive: 0x000000,
|
|
510
|
+
shininess: 100
|
|
511
|
+
});
|
|
512
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
513
|
+
group.add(mesh);
|
|
514
|
+
|
|
515
|
+
// Add holes if specified
|
|
516
|
+
if (spec.features) {
|
|
517
|
+
spec.features.forEach(feature => {
|
|
518
|
+
if (feature.type === 'hole') {
|
|
519
|
+
addHoleToGeometry(group, feature, params);
|
|
520
|
+
} else if (feature.type === 'fillet') {
|
|
521
|
+
// Note: True edge-based fillet approximation would go here
|
|
522
|
+
// For now, visual indicator only
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Apply patterns
|
|
528
|
+
if (spec.features) {
|
|
529
|
+
spec.features.forEach(feature => {
|
|
530
|
+
if (feature.type === 'pattern') {
|
|
531
|
+
applyPatternToGroup(group, feature, spec.relationships);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return group.children.length > 0 ? group : null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Create gear geometry
|
|
542
|
+
* @param {Object} params
|
|
543
|
+
* @returns {THREE.BufferGeometry}
|
|
544
|
+
*/
|
|
545
|
+
function createGearGeometry(params) {
|
|
546
|
+
const teeth = params.teeth || 24;
|
|
547
|
+
const module = params.module || 2;
|
|
548
|
+
const pressureAngle = (params.pressure_angle || 20) * Math.PI / 180;
|
|
549
|
+
|
|
550
|
+
const geometry = new THREE.CylinderGeometry(
|
|
551
|
+
(teeth * module) / 2,
|
|
552
|
+
(teeth * module) / 2,
|
|
553
|
+
module * 2,
|
|
554
|
+
teeth,
|
|
555
|
+
32
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
// Add tooth bumps (simplified)
|
|
559
|
+
const positionAttribute = geometry.getAttribute('position');
|
|
560
|
+
const positions = positionAttribute.array;
|
|
561
|
+
|
|
562
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
563
|
+
const x = positions[i];
|
|
564
|
+
const z = positions[i + 2];
|
|
565
|
+
const dist = Math.sqrt(x * x + z * z);
|
|
566
|
+
const angle = Math.atan2(z, x);
|
|
567
|
+
|
|
568
|
+
// Tooth pattern
|
|
569
|
+
const toothPhase = (angle * teeth / (2 * Math.PI)) % 1;
|
|
570
|
+
if (toothPhase < 0.3) {
|
|
571
|
+
positions[i] *= 1.1;
|
|
572
|
+
positions[i + 2] *= 1.1;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
positionAttribute.needsUpdate = true;
|
|
577
|
+
geometry.computeVertexNormals();
|
|
578
|
+
|
|
579
|
+
return geometry;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Create bracket geometry
|
|
584
|
+
* @param {Object} params
|
|
585
|
+
* @returns {THREE.BufferGeometry}
|
|
586
|
+
*/
|
|
587
|
+
function createBracketGeometry(params) {
|
|
588
|
+
const w = params.width || 60;
|
|
589
|
+
const h = params.height || 100;
|
|
590
|
+
const t = params.thickness || 8;
|
|
591
|
+
|
|
592
|
+
const shape = new THREE.Shape();
|
|
593
|
+
shape.moveTo(0, 0);
|
|
594
|
+
shape.lineTo(w, 0);
|
|
595
|
+
shape.lineTo(w, h * 0.3);
|
|
596
|
+
shape.lineTo(t, h * 0.3);
|
|
597
|
+
shape.lineTo(t, h);
|
|
598
|
+
shape.lineTo(0, h);
|
|
599
|
+
shape.closePath();
|
|
600
|
+
|
|
601
|
+
const geometry = new THREE.ExtrudeGeometry(shape, { depth: t, bevelEnabled: false });
|
|
602
|
+
return geometry;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Create flange geometry
|
|
607
|
+
* @param {Object} params
|
|
608
|
+
* @returns {THREE.BufferGeometry}
|
|
609
|
+
*/
|
|
610
|
+
function createFlangeGeometry(params) {
|
|
611
|
+
const outerDia = params['outer-diameter'] || 100;
|
|
612
|
+
const innerDia = params['inner-diameter'] || 40;
|
|
613
|
+
const thickness = params.thickness || 8;
|
|
614
|
+
|
|
615
|
+
const geometry = new THREE.LatheGeometry(
|
|
616
|
+
[
|
|
617
|
+
new THREE.Vector2(innerDia / 2, 0),
|
|
618
|
+
new THREE.Vector2(outerDia / 2, 0),
|
|
619
|
+
new THREE.Vector2(outerDia / 2, thickness),
|
|
620
|
+
new THREE.Vector2(innerDia / 2, thickness),
|
|
621
|
+
new THREE.Vector2(innerDia / 2, 0)
|
|
622
|
+
],
|
|
623
|
+
32
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
return geometry;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Create housing geometry
|
|
631
|
+
* @param {Object} params
|
|
632
|
+
* @returns {THREE.BufferGeometry}
|
|
633
|
+
*/
|
|
634
|
+
function createHousingGeometry(params) {
|
|
635
|
+
const w = params.width || 100;
|
|
636
|
+
const h = params.height || 80;
|
|
637
|
+
const d = params.depth || 100;
|
|
638
|
+
const wallThickness = params['wall-thickness'] || 5;
|
|
639
|
+
|
|
640
|
+
// Outer box
|
|
641
|
+
const outer = new THREE.BoxGeometry(w, h, d);
|
|
642
|
+
|
|
643
|
+
// Inner box (for subtraction)
|
|
644
|
+
const innerW = w - wallThickness * 2;
|
|
645
|
+
const innerH = h - wallThickness * 2;
|
|
646
|
+
const innerD = d - wallThickness * 2;
|
|
647
|
+
|
|
648
|
+
// For now, just return outer (true CSG would use Boolean)
|
|
649
|
+
return outer;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Add hole feature to geometry
|
|
654
|
+
* @param {THREE.Group} group
|
|
655
|
+
* @param {Object} feature
|
|
656
|
+
* @param {Object} params
|
|
657
|
+
*/
|
|
658
|
+
function addHoleToGeometry(group, feature, params) {
|
|
659
|
+
const holeRadius = feature.diameter / 2;
|
|
660
|
+
const holeDepth = params.height || 50;
|
|
661
|
+
|
|
662
|
+
const holeGeometry = new THREE.CylinderGeometry(
|
|
663
|
+
holeRadius,
|
|
664
|
+
holeRadius,
|
|
665
|
+
holeDepth * 2,
|
|
666
|
+
16
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
const holeMaterial = new THREE.MeshPhongMaterial({
|
|
670
|
+
color: 0x1a1a1a,
|
|
671
|
+
emissive: 0x000000
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const holeMesh = new THREE.Mesh(holeGeometry, holeMaterial);
|
|
675
|
+
holeMesh.position.z = params.pcd || 0;
|
|
676
|
+
group.add(holeMesh);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Apply circular or rectangular pattern to group
|
|
681
|
+
* @param {THREE.Group} group
|
|
682
|
+
* @param {Object} feature
|
|
683
|
+
* @param {Object} relationships
|
|
684
|
+
*/
|
|
685
|
+
function applyPatternToGroup(group, feature, relationships) {
|
|
686
|
+
if (!group.children.length) return;
|
|
687
|
+
|
|
688
|
+
const template = group.children[0];
|
|
689
|
+
const count = feature.count || 4;
|
|
690
|
+
const pcd = relationships.pcd || 70;
|
|
691
|
+
|
|
692
|
+
if (feature.kind === 'circular') {
|
|
693
|
+
const angleStep = (Math.PI * 2) / count;
|
|
694
|
+
|
|
695
|
+
for (let i = 1; i < count; i++) {
|
|
696
|
+
const angle = angleStep * i;
|
|
697
|
+
const x = Math.cos(angle) * (pcd / 2);
|
|
698
|
+
const z = Math.sin(angle) * (pcd / 2);
|
|
699
|
+
|
|
700
|
+
const clone = template.clone();
|
|
701
|
+
clone.position.set(x, 0, z);
|
|
702
|
+
group.add(clone);
|
|
703
|
+
}
|
|
704
|
+
} else if (feature.kind === 'rectangular') {
|
|
705
|
+
const spacing = feature.spacing || 30;
|
|
706
|
+
const countX = feature.countX || 2;
|
|
707
|
+
const countY = feature.countY || 2;
|
|
708
|
+
|
|
709
|
+
for (let x = 0; x < countX; x++) {
|
|
710
|
+
for (let y = 0; y < countY; y++) {
|
|
711
|
+
if (x === 0 && y === 0) continue;
|
|
712
|
+
const clone = template.clone();
|
|
713
|
+
clone.position.set(x * spacing, y * spacing, 0);
|
|
714
|
+
group.add(clone);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ========== LIVE PREVIEW ENGINE (~300 lines) ==========
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Update live preview as user types
|
|
724
|
+
* @param {string} input
|
|
725
|
+
*/
|
|
726
|
+
function updateLivePreview(input) {
|
|
727
|
+
// Clear existing debounce timer
|
|
728
|
+
if (state.parseDebounceTimer) {
|
|
729
|
+
clearTimeout(state.parseDebounceTimer);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Debounce parsing by 300ms
|
|
733
|
+
state.parseDebounceTimer = setTimeout(() => {
|
|
734
|
+
const spec = parseDescription(input);
|
|
735
|
+
|
|
736
|
+
if (spec) {
|
|
737
|
+
// Remove old preview
|
|
738
|
+
if (state.previewGeometry && state.scene) {
|
|
739
|
+
state.scene.remove(state.previewGeometry);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Generate new geometry
|
|
743
|
+
const geometry = generateGeometry(spec);
|
|
744
|
+
|
|
745
|
+
if (geometry) {
|
|
746
|
+
// Make preview semi-transparent and ghostly
|
|
747
|
+
geometry.traverse(mesh => {
|
|
748
|
+
if (mesh.material) {
|
|
749
|
+
mesh.material.opacity = 0.4;
|
|
750
|
+
mesh.material.transparent = true;
|
|
751
|
+
mesh.material.color.setHex(0x00d4ff);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Add to scene
|
|
756
|
+
if (state.scene) {
|
|
757
|
+
state.previewGeometry = geometry;
|
|
758
|
+
state.scene.add(geometry);
|
|
759
|
+
|
|
760
|
+
// Animate camera to view
|
|
761
|
+
fitCameraToObject(geometry);
|
|
762
|
+
|
|
763
|
+
// Update confidence display
|
|
764
|
+
updateConfidenceUI(spec.confidence);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}, 300);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Commit preview to actual geometry
|
|
773
|
+
*/
|
|
774
|
+
function commitPreview() {
|
|
775
|
+
if (!state.previewGeometry) return;
|
|
776
|
+
|
|
777
|
+
// Remove previous geometry
|
|
778
|
+
if (state.currentGeometry && state.scene) {
|
|
779
|
+
state.scene.remove(state.currentGeometry);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Make geometry opaque
|
|
783
|
+
state.previewGeometry.traverse(mesh => {
|
|
784
|
+
if (mesh.material) {
|
|
785
|
+
mesh.material.opacity = 1.0;
|
|
786
|
+
mesh.material.transparent = false;
|
|
787
|
+
mesh.material.color.setHex(0x0284C7);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
state.currentGeometry = state.previewGeometry;
|
|
792
|
+
state.previewGeometry = null;
|
|
793
|
+
|
|
794
|
+
// Add step to history
|
|
795
|
+
addStep({
|
|
796
|
+
input: state.lastParsedInput,
|
|
797
|
+
geometry: state.currentGeometry,
|
|
798
|
+
timestamp: Date.now()
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
return state.currentGeometry;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Add step to history
|
|
806
|
+
* @param {Object} step
|
|
807
|
+
*/
|
|
808
|
+
function addStep(step) {
|
|
809
|
+
state.currentStepIndex++;
|
|
810
|
+
state.steps = state.steps.slice(0, state.currentStepIndex);
|
|
811
|
+
state.steps.push(step);
|
|
812
|
+
updateStepUI();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Undo to previous step
|
|
817
|
+
*/
|
|
818
|
+
function undoStep() {
|
|
819
|
+
if (state.currentStepIndex > 0) {
|
|
820
|
+
state.currentStepIndex--;
|
|
821
|
+
|
|
822
|
+
if (state.scene && state.currentGeometry) {
|
|
823
|
+
state.scene.remove(state.currentGeometry);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const step = state.steps[state.currentStepIndex];
|
|
827
|
+
state.currentGeometry = step.geometry;
|
|
828
|
+
|
|
829
|
+
if (state.scene && state.currentGeometry) {
|
|
830
|
+
state.scene.add(state.currentGeometry);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
updateStepUI();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Redo to next step
|
|
839
|
+
*/
|
|
840
|
+
function redoStep() {
|
|
841
|
+
if (state.currentStepIndex < state.steps.length - 1) {
|
|
842
|
+
state.currentStepIndex++;
|
|
843
|
+
|
|
844
|
+
if (state.scene && state.currentGeometry) {
|
|
845
|
+
state.scene.remove(state.currentGeometry);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const step = state.steps[state.currentStepIndex];
|
|
849
|
+
state.currentGeometry = step.geometry;
|
|
850
|
+
|
|
851
|
+
if (state.scene && state.currentGeometry) {
|
|
852
|
+
state.scene.add(state.currentGeometry);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
updateStepUI();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Fit camera to show geometry
|
|
861
|
+
* @param {THREE.Object3D} object
|
|
862
|
+
*/
|
|
863
|
+
function fitCameraToObject(object) {
|
|
864
|
+
if (!state.renderer || !state.scene) return;
|
|
865
|
+
|
|
866
|
+
const box = new THREE.Box3().setFromObject(object);
|
|
867
|
+
const size = box.getSize(new THREE.Vector3());
|
|
868
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
869
|
+
const fov = 75;
|
|
870
|
+
const distance = maxDim / (2 * Math.tan(fov * Math.PI / 360));
|
|
871
|
+
|
|
872
|
+
// Animate camera
|
|
873
|
+
const currentPos = state.renderer.getCamera ? state.renderer.getCamera().position : { x: 0, y: 0, z: distance };
|
|
874
|
+
const startPos = { ...currentPos };
|
|
875
|
+
const endPos = {
|
|
876
|
+
x: box.getCenter(new THREE.Vector3()).x + distance,
|
|
877
|
+
y: box.getCenter(new THREE.Vector3()).y + distance * 0.7,
|
|
878
|
+
z: box.getCenter(new THREE.Vector3()).z + distance
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
let progress = 0;
|
|
882
|
+
const duration = 400;
|
|
883
|
+
const startTime = Date.now();
|
|
884
|
+
|
|
885
|
+
const animateCamera = () => {
|
|
886
|
+
progress = Math.min(1, (Date.now() - startTime) / duration);
|
|
887
|
+
|
|
888
|
+
if (state.renderer && state.renderer.getCamera) {
|
|
889
|
+
const camera = state.renderer.getCamera();
|
|
890
|
+
camera.position.x = startPos.x + (endPos.x - startPos.x) * progress;
|
|
891
|
+
camera.position.y = startPos.y + (endPos.y - startPos.y) * progress;
|
|
892
|
+
camera.position.z = startPos.z + (endPos.z - startPos.z) * progress;
|
|
893
|
+
camera.lookAt(box.getCenter(new THREE.Vector3()));
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (progress < 1) {
|
|
897
|
+
requestAnimationFrame(animateCamera);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
animateCamera();
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ========== UI FUNCTIONS (~200 lines) ==========
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Get UI panel HTML
|
|
908
|
+
* @returns {HTMLElement}
|
|
909
|
+
*/
|
|
910
|
+
function getUI() {
|
|
911
|
+
const container = document.createElement('div');
|
|
912
|
+
container.className = 'text-to-cad-panel';
|
|
913
|
+
container.innerHTML = `
|
|
914
|
+
<div class="ttc-header">
|
|
915
|
+
<h3>Text-to-CAD</h3>
|
|
916
|
+
<button class="ttc-help-btn" title="Help">?</button>
|
|
917
|
+
</div>
|
|
918
|
+
|
|
919
|
+
<div class="ttc-input-section">
|
|
920
|
+
<textarea
|
|
921
|
+
id="ttc-input"
|
|
922
|
+
class="ttc-input"
|
|
923
|
+
placeholder="e.g., 'a flanged cylinder 50mm diameter, 80mm tall with 4 bolt holes on a 70mm PCD' or 'gear with 24 teeth, module 2'"
|
|
924
|
+
rows="4"
|
|
925
|
+
></textarea>
|
|
926
|
+
<div class="ttc-input-controls">
|
|
927
|
+
<button id="ttc-generate" class="ttc-btn ttc-btn-primary">Generate (Ctrl+Enter)</button>
|
|
928
|
+
<button id="ttc-clear" class="ttc-btn ttc-btn-secondary">Clear</button>
|
|
929
|
+
</div>
|
|
930
|
+
</div>
|
|
931
|
+
|
|
932
|
+
<div class="ttc-preview-section">
|
|
933
|
+
<label class="ttc-checkbox">
|
|
934
|
+
<input id="ttc-live-preview" type="checkbox" checked>
|
|
935
|
+
<span>Live Preview</span>
|
|
936
|
+
</label>
|
|
937
|
+
<div class="ttc-confidence">
|
|
938
|
+
<span>Confidence:</span>
|
|
939
|
+
<div class="ttc-confidence-bar">
|
|
940
|
+
<div id="ttc-confidence-fill" class="ttc-confidence-fill" style="width: 50%"></div>
|
|
941
|
+
</div>
|
|
942
|
+
<span id="ttc-confidence-pct">50%</span>
|
|
943
|
+
</div>
|
|
944
|
+
</div>
|
|
945
|
+
|
|
946
|
+
<div class="ttc-steps-section">
|
|
947
|
+
<h4>Build History</h4>
|
|
948
|
+
<div id="ttc-steps-list" class="ttc-steps-list"></div>
|
|
949
|
+
<div class="ttc-step-controls">
|
|
950
|
+
<button id="ttc-undo" class="ttc-btn ttc-btn-small" title="Undo" disabled>↶ Undo</button>
|
|
951
|
+
<button id="ttc-redo" class="ttc-btn ttc-btn-small" title="Redo" disabled>↷ Redo</button>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
|
|
955
|
+
<div class="ttc-variants-section">
|
|
956
|
+
<h4>Variants</h4>
|
|
957
|
+
<div id="ttc-variants" class="ttc-variants-grid"></div>
|
|
958
|
+
</div>
|
|
959
|
+
|
|
960
|
+
<div class="ttc-examples-section">
|
|
961
|
+
<h4>Example Prompts</h4>
|
|
962
|
+
<div class="ttc-examples">
|
|
963
|
+
<div class="ttc-example" data-prompt="a cylinder 50mm diameter and 80mm tall">Cylinder</div>
|
|
964
|
+
<div class="ttc-example" data-prompt="a gear with 24 teeth and module 2">Gear</div>
|
|
965
|
+
<div class="ttc-example" data-prompt="a plate 100x60x5mm with 2 mounting holes">Plate</div>
|
|
966
|
+
<div class="ttc-example" data-prompt="an L-bracket 100x60x5mm with fillets">Bracket</div>
|
|
967
|
+
<div class="ttc-example" data-prompt="a flanged cylinder with 4 holes on 70mm PCD">Flange</div>
|
|
968
|
+
</div>
|
|
969
|
+
</div>
|
|
970
|
+
`;
|
|
971
|
+
|
|
972
|
+
// Add CSS
|
|
973
|
+
if (!document.querySelector('#ttc-styles')) {
|
|
974
|
+
const style = document.createElement('style');
|
|
975
|
+
style.id = 'ttc-styles';
|
|
976
|
+
style.textContent = getStylesheet();
|
|
977
|
+
document.head.appendChild(style);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return container;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Get CSS stylesheet for panel
|
|
985
|
+
* @returns {string}
|
|
986
|
+
*/
|
|
987
|
+
function getStylesheet() {
|
|
988
|
+
return `
|
|
989
|
+
.text-to-cad-panel {
|
|
990
|
+
display: flex;
|
|
991
|
+
flex-direction: column;
|
|
992
|
+
gap: 12px;
|
|
993
|
+
padding: 12px;
|
|
994
|
+
color: var(--text-primary);
|
|
995
|
+
font-size: 12px;
|
|
996
|
+
background: var(--bg-secondary);
|
|
997
|
+
border-radius: 4px;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.ttc-header {
|
|
1001
|
+
display: flex;
|
|
1002
|
+
justify-content: space-between;
|
|
1003
|
+
align-items: center;
|
|
1004
|
+
border-bottom: 1px solid var(--border-color);
|
|
1005
|
+
padding-bottom: 8px;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.ttc-header h3 {
|
|
1009
|
+
margin: 0;
|
|
1010
|
+
font-size: 14px;
|
|
1011
|
+
font-weight: 600;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.ttc-help-btn {
|
|
1015
|
+
background: var(--bg-tertiary);
|
|
1016
|
+
border: none;
|
|
1017
|
+
color: var(--text-secondary);
|
|
1018
|
+
width: 24px;
|
|
1019
|
+
height: 24px;
|
|
1020
|
+
border-radius: 3px;
|
|
1021
|
+
cursor: pointer;
|
|
1022
|
+
font-size: 12px;
|
|
1023
|
+
transition: all var(--transition-fast);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.ttc-help-btn:hover {
|
|
1027
|
+
background: var(--accent-blue);
|
|
1028
|
+
color: white;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
.ttc-input-section {
|
|
1032
|
+
display: flex;
|
|
1033
|
+
flex-direction: column;
|
|
1034
|
+
gap: 8px;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.ttc-input {
|
|
1038
|
+
background: var(--bg-primary);
|
|
1039
|
+
border: 1px solid var(--border-color);
|
|
1040
|
+
color: var(--text-primary);
|
|
1041
|
+
padding: 8px;
|
|
1042
|
+
border-radius: 3px;
|
|
1043
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
1044
|
+
font-size: 11px;
|
|
1045
|
+
resize: vertical;
|
|
1046
|
+
transition: border-color var(--transition-fast);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.ttc-input:focus {
|
|
1050
|
+
outline: none;
|
|
1051
|
+
border-color: var(--accent-blue);
|
|
1052
|
+
background: var(--bg-primary);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.ttc-input-controls {
|
|
1056
|
+
display: flex;
|
|
1057
|
+
gap: 8px;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.ttc-btn {
|
|
1061
|
+
padding: 6px 12px;
|
|
1062
|
+
border: 1px solid var(--border-color);
|
|
1063
|
+
background: var(--bg-tertiary);
|
|
1064
|
+
color: var(--text-primary);
|
|
1065
|
+
border-radius: 3px;
|
|
1066
|
+
cursor: pointer;
|
|
1067
|
+
font-size: 11px;
|
|
1068
|
+
transition: all var(--transition-fast);
|
|
1069
|
+
flex: 1;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.ttc-btn:hover:not(:disabled) {
|
|
1073
|
+
background: var(--border-color);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.ttc-btn:active:not(:disabled) {
|
|
1077
|
+
background: var(--accent-blue);
|
|
1078
|
+
color: white;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.ttc-btn:disabled {
|
|
1082
|
+
opacity: 0.5;
|
|
1083
|
+
cursor: not-allowed;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
.ttc-btn-primary {
|
|
1087
|
+
background: var(--accent-blue);
|
|
1088
|
+
color: white;
|
|
1089
|
+
border-color: var(--accent-blue);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
.ttc-btn-primary:hover {
|
|
1093
|
+
background: var(--accent-blue-hover);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
.ttc-btn-secondary {
|
|
1097
|
+
flex: 0.5;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
.ttc-btn-small {
|
|
1101
|
+
flex: 0.5;
|
|
1102
|
+
padding: 4px 8px;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
.ttc-preview-section {
|
|
1106
|
+
display: flex;
|
|
1107
|
+
flex-direction: column;
|
|
1108
|
+
gap: 8px;
|
|
1109
|
+
padding: 8px;
|
|
1110
|
+
background: var(--bg-primary);
|
|
1111
|
+
border-radius: 3px;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
.ttc-checkbox {
|
|
1115
|
+
display: flex;
|
|
1116
|
+
align-items: center;
|
|
1117
|
+
gap: 6px;
|
|
1118
|
+
cursor: pointer;
|
|
1119
|
+
user-select: none;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
.ttc-checkbox input {
|
|
1123
|
+
cursor: pointer;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.ttc-confidence {
|
|
1127
|
+
display: flex;
|
|
1128
|
+
align-items: center;
|
|
1129
|
+
gap: 8px;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
.ttc-confidence-bar {
|
|
1133
|
+
flex: 1;
|
|
1134
|
+
height: 6px;
|
|
1135
|
+
background: var(--bg-tertiary);
|
|
1136
|
+
border-radius: 3px;
|
|
1137
|
+
overflow: hidden;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
.ttc-confidence-fill {
|
|
1141
|
+
height: 100%;
|
|
1142
|
+
background: linear-gradient(90deg, var(--accent-red), var(--accent-yellow), var(--accent-green));
|
|
1143
|
+
transition: width 200ms ease-out;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
.ttc-steps-section {
|
|
1147
|
+
display: flex;
|
|
1148
|
+
flex-direction: column;
|
|
1149
|
+
gap: 8px;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
.ttc-steps-section h4 {
|
|
1153
|
+
margin: 0;
|
|
1154
|
+
font-size: 12px;
|
|
1155
|
+
font-weight: 600;
|
|
1156
|
+
color: var(--text-secondary);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
.ttc-steps-list {
|
|
1160
|
+
display: flex;
|
|
1161
|
+
flex-direction: column;
|
|
1162
|
+
gap: 4px;
|
|
1163
|
+
max-height: 150px;
|
|
1164
|
+
overflow-y: auto;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.ttc-step {
|
|
1168
|
+
padding: 6px 8px;
|
|
1169
|
+
background: var(--bg-primary);
|
|
1170
|
+
border-left: 3px solid var(--accent-blue);
|
|
1171
|
+
border-radius: 2px;
|
|
1172
|
+
font-size: 11px;
|
|
1173
|
+
cursor: pointer;
|
|
1174
|
+
transition: all var(--transition-fast);
|
|
1175
|
+
white-space: nowrap;
|
|
1176
|
+
overflow: hidden;
|
|
1177
|
+
text-overflow: ellipsis;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
.ttc-step:hover {
|
|
1181
|
+
background: var(--bg-tertiary);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
.ttc-step.active {
|
|
1185
|
+
background: var(--accent-blue);
|
|
1186
|
+
color: white;
|
|
1187
|
+
border-left-color: white;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
.ttc-step-controls {
|
|
1191
|
+
display: flex;
|
|
1192
|
+
gap: 6px;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
.ttc-variants-section {
|
|
1196
|
+
display: flex;
|
|
1197
|
+
flex-direction: column;
|
|
1198
|
+
gap: 8px;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
.ttc-variants-section h4 {
|
|
1202
|
+
margin: 0;
|
|
1203
|
+
font-size: 12px;
|
|
1204
|
+
font-weight: 600;
|
|
1205
|
+
color: var(--text-secondary);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.ttc-variants-grid {
|
|
1209
|
+
display: grid;
|
|
1210
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1211
|
+
gap: 6px;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
.ttc-variant {
|
|
1215
|
+
aspect-ratio: 1;
|
|
1216
|
+
background: var(--bg-primary);
|
|
1217
|
+
border: 1px solid var(--border-color);
|
|
1218
|
+
border-radius: 3px;
|
|
1219
|
+
cursor: pointer;
|
|
1220
|
+
overflow: hidden;
|
|
1221
|
+
position: relative;
|
|
1222
|
+
transition: all var(--transition-fast);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.ttc-variant:hover {
|
|
1226
|
+
border-color: var(--accent-blue);
|
|
1227
|
+
box-shadow: 0 0 8px rgba(2, 132, 199, 0.3);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.ttc-variant canvas {
|
|
1231
|
+
width: 100%;
|
|
1232
|
+
height: 100%;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
.ttc-examples-section {
|
|
1236
|
+
display: flex;
|
|
1237
|
+
flex-direction: column;
|
|
1238
|
+
gap: 8px;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
.ttc-examples-section h4 {
|
|
1242
|
+
margin: 0;
|
|
1243
|
+
font-size: 12px;
|
|
1244
|
+
font-weight: 600;
|
|
1245
|
+
color: var(--text-secondary);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
.ttc-examples {
|
|
1249
|
+
display: flex;
|
|
1250
|
+
flex-direction: column;
|
|
1251
|
+
gap: 4px;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
.ttc-example {
|
|
1255
|
+
padding: 6px 8px;
|
|
1256
|
+
background: var(--bg-tertiary);
|
|
1257
|
+
border-radius: 3px;
|
|
1258
|
+
cursor: pointer;
|
|
1259
|
+
font-size: 11px;
|
|
1260
|
+
transition: all var(--transition-fast);
|
|
1261
|
+
border: 1px solid transparent;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
.ttc-example:hover {
|
|
1265
|
+
background: var(--border-color);
|
|
1266
|
+
border-color: var(--accent-blue);
|
|
1267
|
+
}
|
|
1268
|
+
`;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Initialize module
|
|
1273
|
+
* @param {THREE.Scene} scene
|
|
1274
|
+
* @param {Object} renderer
|
|
1275
|
+
*/
|
|
1276
|
+
function init(scene, renderer) {
|
|
1277
|
+
state.scene = scene;
|
|
1278
|
+
state.renderer = renderer;
|
|
1279
|
+
|
|
1280
|
+
// Setup event listeners
|
|
1281
|
+
const container = document.querySelector('.text-to-cad-panel');
|
|
1282
|
+
if (!container) return;
|
|
1283
|
+
|
|
1284
|
+
const input = container.querySelector('#ttc-input');
|
|
1285
|
+
const generateBtn = container.querySelector('#ttc-generate');
|
|
1286
|
+
const clearBtn = container.querySelector('#ttc-clear');
|
|
1287
|
+
const livePreviewCheckbox = container.querySelector('#ttc-live-preview');
|
|
1288
|
+
const undoBtn = container.querySelector('#ttc-undo');
|
|
1289
|
+
const redoBtn = container.querySelector('#ttc-redo');
|
|
1290
|
+
const examples = container.querySelectorAll('.ttc-example');
|
|
1291
|
+
|
|
1292
|
+
// Input handling
|
|
1293
|
+
if (input) {
|
|
1294
|
+
input.addEventListener('input', (e) => {
|
|
1295
|
+
if (livePreviewCheckbox && livePreviewCheckbox.checked) {
|
|
1296
|
+
updateLivePreview(e.target.value);
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
input.addEventListener('keydown', (e) => {
|
|
1301
|
+
if (e.ctrlKey && e.key === 'Enter') {
|
|
1302
|
+
generateBtn.click();
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// Generate button
|
|
1308
|
+
if (generateBtn) {
|
|
1309
|
+
generateBtn.addEventListener('click', () => {
|
|
1310
|
+
if (input) {
|
|
1311
|
+
const spec = parseDescription(input.value);
|
|
1312
|
+
if (spec) {
|
|
1313
|
+
const geometry = generateGeometry(spec);
|
|
1314
|
+
if (geometry) {
|
|
1315
|
+
commitPreview();
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Clear button
|
|
1323
|
+
if (clearBtn) {
|
|
1324
|
+
clearBtn.addEventListener('click', () => {
|
|
1325
|
+
if (input) input.value = '';
|
|
1326
|
+
if (state.previewGeometry && state.scene) {
|
|
1327
|
+
state.scene.remove(state.previewGeometry);
|
|
1328
|
+
state.previewGeometry = null;
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Undo/Redo
|
|
1334
|
+
if (undoBtn) {
|
|
1335
|
+
undoBtn.addEventListener('click', undoStep);
|
|
1336
|
+
}
|
|
1337
|
+
if (redoBtn) {
|
|
1338
|
+
redoBtn.addEventListener('click', redoStep);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Example prompts
|
|
1342
|
+
examples.forEach(example => {
|
|
1343
|
+
example.addEventListener('click', () => {
|
|
1344
|
+
if (input) {
|
|
1345
|
+
input.value = example.dataset.prompt;
|
|
1346
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
// Live preview toggle
|
|
1352
|
+
if (livePreviewCheckbox) {
|
|
1353
|
+
livePreviewCheckbox.addEventListener('change', (e) => {
|
|
1354
|
+
if (!e.target.checked && state.previewGeometry && state.scene) {
|
|
1355
|
+
state.scene.remove(state.previewGeometry);
|
|
1356
|
+
state.previewGeometry = null;
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
console.log('TextToCAD module initialized');
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Execute command from API
|
|
1366
|
+
* @param {string} command
|
|
1367
|
+
* @param {Object} params
|
|
1368
|
+
* @returns {any}
|
|
1369
|
+
*/
|
|
1370
|
+
function execute(command, params) {
|
|
1371
|
+
switch (command) {
|
|
1372
|
+
case 'parse':
|
|
1373
|
+
return parseDescription(params.input);
|
|
1374
|
+
case 'generate':
|
|
1375
|
+
return generateGeometry(params.spec);
|
|
1376
|
+
case 'preview':
|
|
1377
|
+
updateLivePreview(params.input);
|
|
1378
|
+
return state.previewGeometry;
|
|
1379
|
+
case 'commit':
|
|
1380
|
+
return commitPreview();
|
|
1381
|
+
case 'undo':
|
|
1382
|
+
undoStep();
|
|
1383
|
+
return state.currentGeometry;
|
|
1384
|
+
case 'redo':
|
|
1385
|
+
redoStep();
|
|
1386
|
+
return state.currentGeometry;
|
|
1387
|
+
case 'getHistory':
|
|
1388
|
+
return state.steps;
|
|
1389
|
+
default:
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* Update UI elements for confidence score
|
|
1396
|
+
* @param {number} confidence
|
|
1397
|
+
*/
|
|
1398
|
+
function updateConfidenceUI(confidence) {
|
|
1399
|
+
const fill = document.querySelector('#ttc-confidence-fill');
|
|
1400
|
+
const pct = document.querySelector('#ttc-confidence-pct');
|
|
1401
|
+
|
|
1402
|
+
if (fill) {
|
|
1403
|
+
fill.style.width = (confidence * 100) + '%';
|
|
1404
|
+
}
|
|
1405
|
+
if (pct) {
|
|
1406
|
+
pct.textContent = Math.round(confidence * 100) + '%';
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Update step history UI
|
|
1412
|
+
*/
|
|
1413
|
+
function updateStepUI() {
|
|
1414
|
+
const stepsList = document.querySelector('#ttc-steps-list');
|
|
1415
|
+
const undoBtn = document.querySelector('#ttc-undo');
|
|
1416
|
+
const redoBtn = document.querySelector('#ttc-redo');
|
|
1417
|
+
|
|
1418
|
+
if (!stepsList) return;
|
|
1419
|
+
|
|
1420
|
+
stepsList.innerHTML = '';
|
|
1421
|
+
state.steps.forEach((step, idx) => {
|
|
1422
|
+
const stepEl = document.createElement('div');
|
|
1423
|
+
stepEl.className = 'ttc-step';
|
|
1424
|
+
if (idx === state.currentStepIndex) {
|
|
1425
|
+
stepEl.classList.add('active');
|
|
1426
|
+
}
|
|
1427
|
+
stepEl.textContent = `Step ${idx + 1}: ${step.input.substring(0, 40)}...`;
|
|
1428
|
+
stepEl.addEventListener('click', () => {
|
|
1429
|
+
state.currentStepIndex = idx;
|
|
1430
|
+
if (state.scene && state.currentGeometry) {
|
|
1431
|
+
state.scene.remove(state.currentGeometry);
|
|
1432
|
+
}
|
|
1433
|
+
state.currentGeometry = step.geometry;
|
|
1434
|
+
if (state.scene) {
|
|
1435
|
+
state.scene.add(state.currentGeometry);
|
|
1436
|
+
}
|
|
1437
|
+
updateStepUI();
|
|
1438
|
+
});
|
|
1439
|
+
stepsList.appendChild(stepEl);
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
if (undoBtn) {
|
|
1443
|
+
undoBtn.disabled = state.currentStepIndex <= 0;
|
|
1444
|
+
}
|
|
1445
|
+
if (redoBtn) {
|
|
1446
|
+
redoBtn.disabled = state.currentStepIndex >= state.steps.length - 1;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// ========== MODULE EXPORT ==========
|
|
1451
|
+
|
|
1452
|
+
window.CycleCAD = window.CycleCAD || {};
|
|
1453
|
+
window.CycleCAD.TextToCAD = {
|
|
1454
|
+
init,
|
|
1455
|
+
getUI,
|
|
1456
|
+
execute,
|
|
1457
|
+
parseDescription,
|
|
1458
|
+
generateGeometry,
|
|
1459
|
+
state: () => state
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
console.log('TextToCAD module loaded');
|
|
1463
|
+
|
|
1464
|
+
})();
|