cyclecad 1.2.0 → 1.3.1

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.
@@ -0,0 +1,585 @@
1
+ /**
2
+ * Text-to-BREP Engine for cycleCAD
3
+ * Converts natural language descriptions to OpenCascade.js B-rep commands
4
+ *
5
+ * Supports:
6
+ * - Gemini Flash API (primary)
7
+ * - Offline pattern matching (fallback)
8
+ * - 10+ predefined templates
9
+ */
10
+
11
+ const TEMPLATES = {
12
+ bearing_housing: {
13
+ name: "Flanged Bearing Housing",
14
+ description: "Cylinder with bore, flange, and bolt holes",
15
+ defaults: { od: 80, id: 50, bore: 30, flange_width: 15, flange_height: 120, holes: 4, hole_diameter: 8 },
16
+ commands: (p) => [
17
+ { op: "cylinder", radius: p.od / 2, height: p.flange_height },
18
+ { op: "cylinder", radius: p.bore / 2, height: p.flange_height + 5, x: 0, y: 0, z: -2.5, mode: "cut" },
19
+ { op: "hole", radius: p.hole_diameter / 2, depth: p.flange_width, count: p.holes, spread: p.od - 10 }
20
+ ]
21
+ },
22
+ motor_mount: {
23
+ name: "Motor Mount Plate",
24
+ description: "Rectangular plate with corner holes and center bore",
25
+ defaults: { width: 150, height: 100, depth: 10, hole_diameter: 8, fillet: 3, bore_diameter: 25 },
26
+ commands: (p) => [
27
+ { op: "box", width: p.width, height: p.height, depth: p.depth },
28
+ { op: "hole", radius: p.hole_diameter / 2, depth: p.depth + 2, x: p.width / 2 - 20, y: p.height / 2 - 20, count: 4, spread: 40 },
29
+ { op: "hole", radius: p.bore_diameter / 2, depth: p.depth + 2, x: 0, y: 0 },
30
+ { op: "fillet", radius: p.fillet }
31
+ ]
32
+ },
33
+ bracket: {
34
+ name: "L-Bracket",
35
+ description: "L-shaped bracket with mounting holes",
36
+ defaults: { width: 100, height: 80, depth: 60, arm_width: 20, hole_diameter: 6, fillet: 2 },
37
+ commands: (p) => [
38
+ { op: "box", width: p.width, height: p.arm_width, depth: p.depth },
39
+ { op: "box", width: p.arm_width, height: p.height, depth: p.depth, x: 0, y: p.height / 2, mode: "add" },
40
+ { op: "hole", radius: p.hole_diameter / 2, depth: p.depth + 2, x: p.width / 2, y: p.arm_width / 2, count: 3 },
41
+ { op: "fillet", radius: p.fillet }
42
+ ]
43
+ },
44
+ gear: {
45
+ name: "Spur Gear",
46
+ description: "Cylinder with approximated teeth and bore",
47
+ defaults: { od: 100, bore: 20, teeth: 20, depth: 25, fillet: 1 },
48
+ commands: (p) => [
49
+ { op: "cylinder", radius: p.od / 2, height: p.depth },
50
+ { op: "cylinder", radius: (p.od / 2 - 5), height: p.depth + 2, x: 0, y: 0, z: -1, mode: "cut" },
51
+ { op: "hole", radius: p.bore / 2, depth: p.depth + 2, x: 0, y: 0 },
52
+ { op: "pattern", type: "circular", count: p.teeth, radius: p.od / 2 - 2 }
53
+ ]
54
+ },
55
+ flange: {
56
+ name: "Pipe Flange",
57
+ description: "Circular flange with central bore and bolt circle",
58
+ defaults: { od: 120, id: 50, thickness: 15, bore: 30, bolt_holes: 4, bolt_diameter: 8 },
59
+ commands: (p) => [
60
+ { op: "cylinder", radius: p.od / 2, height: p.thickness },
61
+ { op: "hole", radius: p.bore / 2, depth: p.thickness + 2, x: 0, y: 0 },
62
+ { op: "hole", radius: p.bolt_diameter / 2, depth: p.thickness + 2, count: p.bolt_holes, spread: p.od - 20 },
63
+ { op: "chamfer", radius: 1.5 }
64
+ ]
65
+ },
66
+ shaft_collar: {
67
+ name: "Shaft Collar",
68
+ description: "Split collar with set screw",
69
+ defaults: { od: 40, id: 25, width: 30, slot_width: 5, set_screw_diameter: 4 },
70
+ commands: (p) => [
71
+ { op: "cylinder", radius: p.od / 2, height: p.width },
72
+ { op: "hole", radius: p.id / 2, depth: p.width + 2, x: 0, y: 0 },
73
+ { op: "box", width: p.slot_width, height: p.od + 5, depth: p.width + 2, x: 0, y: (p.od / 2) + 2.5, mode: "cut" },
74
+ { op: "hole", radius: p.set_screw_diameter / 2, depth: p.od / 2 + 2 }
75
+ ]
76
+ },
77
+ panel: {
78
+ name: "Control Panel",
79
+ description: "Rectangular panel with mounting holes and indicator holes",
80
+ defaults: { width: 200, height: 150, depth: 2, corner_holes: 4, corner_radius: 10, indicator_holes: 6, indicator_diameter: 12 },
81
+ commands: (p) => [
82
+ { op: "box", width: p.width, height: p.height, depth: p.depth },
83
+ { op: "hole", radius: 3, depth: p.depth + 1, count: p.corner_holes, spread: Math.min(p.width, p.height) - 20 },
84
+ { op: "hole", radius: p.indicator_diameter / 2, depth: p.depth + 1, count: p.indicator_holes, spread: p.width - 40 },
85
+ { op: "fillet", radius: p.corner_radius }
86
+ ]
87
+ },
88
+ connector: {
89
+ name: "Cylindrical Connector",
90
+ description: "Connector with threads, grooves, and hex recess",
91
+ defaults: { od: 50, threads: 20, thread_depth: 2, length: 60, hex_width: 10, recess_depth: 8 },
92
+ commands: (p) => [
93
+ { op: "cylinder", radius: p.od / 2, height: p.length },
94
+ { op: "pattern", type: "helical", radius: (p.od / 2 - 2), turns: p.threads, depth: p.thread_depth },
95
+ { op: "box", width: p.hex_width, height: p.hex_width * 0.87, depth: p.recess_depth, x: 0, y: 0, z: 0, mode: "cut" },
96
+ { op: "chamfer", radius: 1 }
97
+ ]
98
+ },
99
+ enclosure: {
100
+ name: "Equipment Enclosure",
101
+ description: "Box enclosure with ventilation holes and mounting feet",
102
+ defaults: { width: 300, depth: 200, height: 150, wall_thickness: 5, vent_holes: 12, foot_height: 20 },
103
+ commands: (p) => [
104
+ { op: "box", width: p.width, height: p.height, depth: p.depth },
105
+ { op: "box", width: p.width - p.wall_thickness * 2, height: p.height - p.wall_thickness * 2, depth: p.depth - p.wall_thickness * 2, x: 0, y: 0, z: p.wall_thickness, mode: "cut" },
106
+ { op: "hole", radius: 5, depth: p.wall_thickness + 2, count: p.vent_holes, spread: p.width - 40 },
107
+ { op: "box", width: 30, height: 20, depth: p.foot_height, x: -p.width / 2 + 20, y: -p.depth / 2 + 20, mode: "add", count: 4 }
108
+ ]
109
+ },
110
+ bearing_block: {
111
+ name: "Pillow Block Bearing",
112
+ description: "Rectangular block with bearing bore and mounting holes",
113
+ defaults: { width: 120, height: 100, depth: 80, bore_diameter: 50, hole_diameter: 10, fillet: 4 },
114
+ commands: (p) => [
115
+ { op: "box", width: p.width, height: p.height, depth: p.depth },
116
+ { op: "cylinder", radius: p.bore_diameter / 2, height: p.depth + 2, x: 0, y: p.height / 4 + 10, mode: "cut" },
117
+ { op: "hole", radius: p.hole_diameter / 2, depth: p.width + 2, count: 4, spread: p.height - 30 },
118
+ { op: "fillet", radius: p.fillet }
119
+ ]
120
+ }
121
+ };
122
+
123
+ const OFFLINE_PATTERNS = [
124
+ {
125
+ pattern: /(\d+)\s*(?:mm\s)?(?:cube|cubic)/i,
126
+ handler: (match) => ({
127
+ commands: [{ op: "box", width: parseFloat(match[1]), height: parseFloat(match[1]), depth: parseFloat(match[1]) }],
128
+ description: `${match[1]}mm cube`
129
+ })
130
+ },
131
+ {
132
+ pattern: /(?:plate|block)\s+(\d+)\s*x\s*(\d+)\s*x\s*(\d+)/i,
133
+ handler: (match) => ({
134
+ commands: [{ op: "box", width: parseFloat(match[1]), height: parseFloat(match[2]), depth: parseFloat(match[3]) }],
135
+ description: `${match[1]}×${match[2]}×${match[3]}mm plate`
136
+ })
137
+ },
138
+ {
139
+ pattern: /cylinder\s+(?:radius|r)\s*(\d+)\s+(?:height|h)\s*(\d+)/i,
140
+ handler: (match) => ({
141
+ commands: [{ op: "cylinder", radius: parseFloat(match[1]), height: parseFloat(match[2]) }],
142
+ description: `Cylinder R${match[1]}×H${match[2]}mm`
143
+ })
144
+ },
145
+ {
146
+ pattern: /(?:with\s+)?(\d+)\s+(?:m\d+\s+)?(?:bolt\s+)?holes?/i,
147
+ handler: (match) => ({ addCommand: { op: "hole", radius: 4, depth: 10, count: parseInt(match[1]), spread: 40 } })
148
+ },
149
+ {
150
+ pattern: /(?:with\s+)?(\d+)\s*mm\s+fillets?(?:\s+on\s+all\s+edges)?/i,
151
+ handler: (match) => ({ addCommand: { op: "fillet", radius: parseFloat(match[1]) } })
152
+ },
153
+ {
154
+ pattern: /(?:with\s+)?(\d+)\s*mm\s+chamfers?/i,
155
+ handler: (match) => ({ addCommand: { op: "chamfer", radius: parseFloat(match[1]) } })
156
+ },
157
+ {
158
+ pattern: /bearing\s+housing/i,
159
+ handler: () => ({ template: "bearing_housing" })
160
+ },
161
+ {
162
+ pattern: /motor\s+mount\s+plate/i,
163
+ handler: () => ({ template: "motor_mount" })
164
+ },
165
+ {
166
+ pattern: /l[‐-]?bracket/i,
167
+ handler: () => ({ template: "bracket" })
168
+ },
169
+ {
170
+ pattern: /spur\s+gear/i,
171
+ handler: () => ({ template: "gear" })
172
+ },
173
+ {
174
+ pattern: /pipe\s+flange/i,
175
+ handler: () => ({ template: "flange" })
176
+ },
177
+ {
178
+ pattern: /shaft\s+collar/i,
179
+ handler: () => ({ template: "shaft_collar" })
180
+ },
181
+ {
182
+ pattern: /control\s+panel/i,
183
+ handler: () => ({ template: "panel" })
184
+ },
185
+ {
186
+ pattern: /connector/i,
187
+ handler: () => ({ template: "connector" })
188
+ },
189
+ {
190
+ pattern: /enclosure/i,
191
+ handler: () => ({ template: "enclosure" })
192
+ },
193
+ {
194
+ pattern: /bearing\s+block|pillow\s+block/i,
195
+ handler: () => ({ template: "bearing_block" })
196
+ }
197
+ ];
198
+
199
+ const SYSTEM_PROMPT = `You are a CAD command generator. Convert natural language descriptions into a JSON array of OpenCascade.js B-rep operations.
200
+
201
+ Output ONLY valid JSON array, no markdown, no explanation, no code blocks.
202
+
203
+ Available operations:
204
+ - { "op": "box", "width": num, "height": num, "depth": num, "x": num, "y": num, "z": num, "mode": "add"|"cut" }
205
+ - { "op": "cylinder", "radius": num, "height": num, "x": num, "y": num, "z": num, "mode": "add"|"cut" }
206
+ - { "op": "sphere", "radius": num, "x": num, "y": num, "z": num, "mode": "add"|"cut" }
207
+ - { "op": "cone", "radius": num, "height": num, "x": num, "y": num, "z": num, "mode": "add"|"cut" }
208
+ - { "op": "torus", "major_radius": num, "minor_radius": num, "x": num, "y": num, "z": num, "mode": "add"|"cut" }
209
+ - { "op": "hole", "radius": num, "depth": num, "x": num, "y": num, "z": num, "count": num, "spread": num }
210
+ - { "op": "fillet", "radius": num }
211
+ - { "op": "chamfer", "radius": num }
212
+ - { "op": "translate", "x": num, "y": num, "z": num }
213
+ - { "op": "pattern", "type": "rectangular"|"circular"|"helical", "count": num, "spacing": num, "radius": num }
214
+
215
+ Rules:
216
+ 1. Start with primary shape (box, cylinder, etc.)
217
+ 2. Use mode "cut" for subtractive operations (holes, slots)
218
+ 3. Use mode "add" for additive operations (flanges, bosses)
219
+ 4. Add fillets/chamfers at the end
220
+ 5. Estimate reasonable dimensions if not specified
221
+ 6. Group similar operations together
222
+
223
+ Example input: "100x80x20mm plate with 4 M8 bolt holes at corners and 3mm fillets"
224
+ Example output: [{"op":"box","width":100,"height":80,"depth":20},{"op":"hole","radius":4,"depth":22,"count":4,"spread":60},{"op":"fillet","radius":3}]`;
225
+
226
+ /**
227
+ * Parse API key from localStorage
228
+ */
229
+ function getGeminiKey() {
230
+ try {
231
+ const keys = localStorage.getItem('explodeview_api_keys');
232
+ if (!keys) return null;
233
+ const parsed = JSON.parse(keys);
234
+ return parsed.gemini || null;
235
+ } catch (e) {
236
+ return null;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Call Gemini Flash API to generate B-rep commands
242
+ */
243
+ async function callGeminiAPI(userText, apiKey) {
244
+ try {
245
+ const response = await fetch(
246
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
247
+ {
248
+ method: 'POST',
249
+ headers: { 'Content-Type': 'application/json' },
250
+ body: JSON.stringify({
251
+ contents: [{
252
+ parts: [{
253
+ text: `${SYSTEM_PROMPT}\n\nUser: ${userText}`
254
+ }]
255
+ }],
256
+ generationConfig: { temperature: 0.1, maxOutputTokens: 1000 }
257
+ })
258
+ }
259
+ );
260
+
261
+ if (!response.ok) {
262
+ console.warn('[Text-to-BREP] Gemini API error:', response.status);
263
+ return null;
264
+ }
265
+
266
+ const data = await response.json();
267
+ const content = data?.candidates?.[0]?.content?.parts?.[0]?.text;
268
+ if (!content) return null;
269
+
270
+ // Try to parse JSON from response
271
+ const jsonMatch = content.match(/\[[\s\S]*\]/);
272
+ if (!jsonMatch) return null;
273
+
274
+ return JSON.parse(jsonMatch[0]);
275
+ } catch (e) {
276
+ console.warn('[Text-to-BREP] Gemini API call failed:', e.message);
277
+ return null;
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Offline pattern matching parser
283
+ */
284
+ function parseOfflinePatterns(text) {
285
+ const commands = [];
286
+ let description = text.substring(0, 50);
287
+ let foundTemplate = null;
288
+
289
+ // Check for templates first
290
+ for (const [key, template] of Object.entries(TEMPLATES)) {
291
+ if (new RegExp(template.name, 'i').test(text) ||
292
+ new RegExp(key.replace(/_/g, '\\s+'), 'i').test(text)) {
293
+ foundTemplate = key;
294
+ description = template.name;
295
+ break;
296
+ }
297
+ }
298
+
299
+ if (foundTemplate) {
300
+ const template = TEMPLATES[foundTemplate];
301
+ const params = { ...template.defaults };
302
+
303
+ // Extract override dimensions from text
304
+ const dimMatch = text.match(/(\d+)\s*x\s*(\d+)\s*x\s*(\d+)/i);
305
+ if (dimMatch) {
306
+ params.width = parseFloat(dimMatch[1]);
307
+ params.height = parseFloat(dimMatch[2]);
308
+ params.depth = parseFloat(dimMatch[3]);
309
+ }
310
+
311
+ // Extract hole count
312
+ const holeMatch = text.match(/(\d+)\s+(?:m\d+\s+)?(?:bolt\s+)?holes?/i);
313
+ if (holeMatch) params.holes = parseInt(holeMatch[1]);
314
+
315
+ // Extract fillet radius
316
+ const filletMatch = text.match(/(\d+)\s*mm\s+fillets?/i);
317
+ if (filletMatch) params.fillet = parseFloat(filletMatch[1]);
318
+
319
+ return {
320
+ commands: template.commands(params),
321
+ description,
322
+ template: foundTemplate
323
+ };
324
+ }
325
+
326
+ // Try pattern matching
327
+ for (const patternObj of OFFLINE_PATTERNS) {
328
+ const match = text.match(patternObj.pattern);
329
+ if (match) {
330
+ const result = patternObj.handler(match);
331
+
332
+ if (result.template) {
333
+ return parseOfflinePatterns(result.template);
334
+ } else if (result.addCommand) {
335
+ commands.push(result.addCommand);
336
+ } else if (result.commands) {
337
+ commands.push(...result.commands);
338
+ description = result.description;
339
+ }
340
+ }
341
+ }
342
+
343
+ // If nothing matched, return a simple box as default
344
+ if (commands.length === 0) {
345
+ commands.push({
346
+ op: "box",
347
+ width: 100,
348
+ height: 100,
349
+ depth: 50
350
+ });
351
+ description = "Default box (no pattern matched)";
352
+ }
353
+
354
+ return { commands, description };
355
+ }
356
+
357
+ /**
358
+ * Execute B-rep commands to create mesh
359
+ */
360
+ async function executeBRepCommands(commands) {
361
+ // Check if brep engine is available
362
+ if (!window.brepEngine) {
363
+ console.warn('[Text-to-BREP] BREPEngine not found, using Three.js fallback');
364
+ return null;
365
+ }
366
+
367
+ try {
368
+ let shape = null;
369
+ let baseShape = null;
370
+
371
+ for (const cmd of commands) {
372
+ switch (cmd.op) {
373
+ case 'box':
374
+ shape = window.brepEngine.makeBox(cmd.width || 100, cmd.height || 100, cmd.depth || 50);
375
+ if (!baseShape) baseShape = shape;
376
+ break;
377
+
378
+ case 'cylinder':
379
+ const cyl = window.brepEngine.makeCylinder(
380
+ cmd.radius || 25,
381
+ cmd.height || 50,
382
+ cmd.x || 0,
383
+ cmd.y || 0,
384
+ cmd.z || 0
385
+ );
386
+ if (cmd.mode === 'cut' && shape) {
387
+ shape = window.brepEngine.booleanCut(shape, cyl);
388
+ } else if (cmd.mode === 'add' && shape) {
389
+ shape = window.brepEngine.booleanUnion(shape, cyl);
390
+ } else {
391
+ shape = cyl;
392
+ if (!baseShape) baseShape = shape;
393
+ }
394
+ break;
395
+
396
+ case 'hole':
397
+ if (shape) {
398
+ for (let i = 0; i < (cmd.count || 1); i++) {
399
+ const angle = (i / (cmd.count || 1)) * Math.PI * 2;
400
+ const x = (cmd.x || 0) + Math.cos(angle) * (cmd.spread || 0) / 2;
401
+ const y = (cmd.y || 0) + Math.sin(angle) * (cmd.spread || 0) / 2;
402
+ const hole = window.brepEngine.makeCylinder(
403
+ cmd.radius || 5,
404
+ cmd.depth || 20,
405
+ x,
406
+ y,
407
+ cmd.z || -10
408
+ );
409
+ shape = window.brepEngine.booleanCut(shape, hole);
410
+ }
411
+ }
412
+ break;
413
+
414
+ case 'fillet':
415
+ if (shape && window.brepEngine.filletAll) {
416
+ shape = window.brepEngine.filletAll(shape, cmd.radius || 2);
417
+ }
418
+ break;
419
+
420
+ case 'chamfer':
421
+ if (shape && window.brepEngine.chamferAll) {
422
+ shape = window.brepEngine.chamferAll(shape, cmd.radius || 1.5);
423
+ }
424
+ break;
425
+
426
+ case 'translate':
427
+ if (shape && window.brepEngine.translate) {
428
+ shape = window.brepEngine.translate(shape, cmd.x || 0, cmd.y || 0, cmd.z || 0);
429
+ }
430
+ break;
431
+
432
+ case 'pattern':
433
+ // Pattern is more complex, would need brepEngine support
434
+ console.log('[Text-to-BREP] Pattern operation skipped (requires brepEngine support)');
435
+ break;
436
+ }
437
+ }
438
+
439
+ return shape || baseShape;
440
+ } catch (e) {
441
+ console.error('[Text-to-BREP] BRep execution error:', e.message);
442
+ return null;
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Create a Three.js mesh from a B-rep shape
448
+ */
449
+ function shapeToMesh(shape) {
450
+ if (!shape) return null;
451
+
452
+ try {
453
+ // If shape has a toMesh method, use it
454
+ if (typeof shape.toMesh === 'function') {
455
+ return shape.toMesh();
456
+ }
457
+
458
+ // Otherwise, create a simple Three.js geometry from the shape
459
+ // This is a placeholder for actual B-rep → mesh conversion
460
+ const geometry = new THREE.BufferGeometry();
461
+ const vertices = new Float32Array([
462
+ -50, -50, -50, 50, -50, -50, 50, 50, -50, -50, 50, -50,
463
+ -50, -50, 50, 50, -50, 50, 50, 50, 50, -50, 50, 50
464
+ ]);
465
+ const indices = new Uint16Array([
466
+ 0,1,2, 0,2,3, 4,6,5, 4,7,6, 0,4,5, 0,5,1,
467
+ 2,6,7, 2,7,3, 0,3,7, 0,7,4, 1,5,6, 1,6,2
468
+ ]);
469
+
470
+ geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
471
+ geometry.setIndex(new THREE.BufferAttribute(indices, 1));
472
+ geometry.computeVertexNormals();
473
+
474
+ const material = new THREE.MeshPhongMaterial({ color: 0x2563eb, shininess: 100 });
475
+ return new THREE.Mesh(geometry, material);
476
+ } catch (e) {
477
+ console.error('[Text-to-BREP] Shape to mesh conversion failed:', e.message);
478
+ return null;
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Main function: Convert text to B-rep commands and mesh
484
+ */
485
+ export async function textToBRep(text) {
486
+ if (!text || text.trim().length === 0) {
487
+ return { commands: [], mesh: null, wireframe: null, description: "Empty input" };
488
+ }
489
+
490
+ console.log('[Text-to-BREP] Input:', text);
491
+
492
+ let commands = null;
493
+ let description = "";
494
+ let source = "offline";
495
+
496
+ // Try Gemini API first
497
+ const apiKey = getGeminiKey();
498
+ if (apiKey) {
499
+ console.log('[Text-to-BREP] Attempting Gemini API...');
500
+ commands = await callGeminiAPI(text, apiKey);
501
+ if (commands) {
502
+ source = "gemini";
503
+ description = `Generated via Gemini: ${text.substring(0, 50)}`;
504
+ }
505
+ }
506
+
507
+ // Fall back to offline parser
508
+ if (!commands) {
509
+ console.log('[Text-to-BREP] Using offline parser');
510
+ const result = parseOfflinePatterns(text);
511
+ commands = result.commands;
512
+ description = result.description;
513
+ }
514
+
515
+ console.log('[Text-to-BREP] Commands:', commands);
516
+
517
+ // Execute B-rep commands
518
+ const shape = await executeBRepCommands(commands);
519
+ const mesh = shapeToMesh(shape);
520
+
521
+ // Create wireframe mesh
522
+ let wireframe = null;
523
+ if (mesh) {
524
+ const wireframeGeometry = new THREE.WireframeGeometry(mesh.geometry);
525
+ const wireframeMaterial = new THREE.LineBasicMaterial({ color: 0x0ea5e9, linewidth: 1 });
526
+ wireframe = new THREE.LineSegments(wireframeGeometry, wireframeMaterial);
527
+ }
528
+
529
+ return {
530
+ commands,
531
+ mesh,
532
+ wireframe,
533
+ description,
534
+ source
535
+ };
536
+ }
537
+
538
+ /**
539
+ * Helper: Get all available templates
540
+ */
541
+ export function getAvailableTemplates() {
542
+ return Object.entries(TEMPLATES).map(([key, template]) => ({
543
+ id: key,
544
+ name: template.name,
545
+ description: template.description,
546
+ defaults: template.defaults
547
+ }));
548
+ }
549
+
550
+ /**
551
+ * Helper: Create mesh from template
552
+ */
553
+ export async function templateToBRep(templateId, overrides = {}) {
554
+ const template = TEMPLATES[templateId];
555
+ if (!template) {
556
+ console.error('[Text-to-BREP] Template not found:', templateId);
557
+ return null;
558
+ }
559
+
560
+ const params = { ...template.defaults, ...overrides };
561
+ const commands = template.commands(params);
562
+
563
+ console.log('[Text-to-BREP] Template:', templateId, commands);
564
+
565
+ const shape = await executeBRepCommands(commands);
566
+ const mesh = shapeToMesh(shape);
567
+
568
+ return {
569
+ commands,
570
+ mesh,
571
+ description: template.name,
572
+ template: templateId
573
+ };
574
+ }
575
+
576
+ /**
577
+ * Register on window
578
+ */
579
+ if (typeof window !== 'undefined') {
580
+ window.textToBRep = textToBRep;
581
+ window.textToBRepTemplates = getAvailableTemplates;
582
+ window.templateToBRep = templateToBRep;
583
+ }
584
+
585
+ export default { textToBRep, getAvailableTemplates, templateToBRep, TEMPLATES, OFFLINE_PATTERNS };