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.
- package/.github/scripts/cad-diff.js +590 -0
- package/.github/workflows/cad-diff.yml +117 -0
- package/KILLER-README.md +377 -0
- package/README.md +354 -35
- package/app/index.html +86 -31
- package/app/js/ai-chat.js +3 -0
- package/app/js/cad-vr.js +1 -0
- package/app/js/multiplayer.js +465 -0
- package/app/js/parts-library.js +778 -0
- package/app/js/step-viewer.js +584 -0
- package/app/js/text-to-brep.js +585 -0
- package/docs/ARCHITECTURE.html +1429 -0
- package/package.json +1 -1
|
@@ -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 };
|