cyclecad 3.9.14 → 3.9.18

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,643 @@
1
+ /**
2
+ * SCAD Export Module for cycleCAD
3
+ * Three.js ↔ OpenSCAD conversion with BOSL2/MCAD library support
4
+ *
5
+ * Exports cycleCAD designs to parametric OpenSCAD code
6
+ * Imports OpenSCAD designs to Three.js geometry
7
+ * Includes 50+ BOSL2 shapes and MCAD library definitions
8
+ */
9
+
10
+ const CycleCADSCADExport = (() => {
11
+ // ============================================================================
12
+ // STATE & CONFIG
13
+ // ============================================================================
14
+
15
+ let scene = null;
16
+ let camera = null;
17
+ const SCAD_PRIMITIVES = {
18
+ 'BoxGeometry': 'cube',
19
+ 'CylinderGeometry': 'cylinder',
20
+ 'SphereGeometry': 'sphere',
21
+ 'TorusGeometry': 'torus',
22
+ 'ConeGeometry': 'cone',
23
+ 'TetrahedronGeometry': 'tetrahedron',
24
+ 'OctahedronGeometry': 'octahedron',
25
+ 'DodecahedronGeometry': 'dodecahedron',
26
+ 'IcosahedronGeometry': 'icosahedron'
27
+ };
28
+
29
+ const BOSL2_SHAPES = {
30
+ 'screw': { params: 'spec, head, shaft, length, pitch, thread, rotate, anchor, orient', desc: 'Metric screw with head' },
31
+ 'nut': { params: 'spec, hole, thickness, anchor, orient', desc: 'Metric nut' },
32
+ 'bolt_head': { params: 'spec, length, anchor, orient', desc: 'Bolt head only' },
33
+ 'spur_gear': { params: 'pitch, teeth, thickness, shaft_diam, hide_root, center, anchor, orient', desc: 'Spur gear' },
34
+ 'bevel_gear': { params: 'pitch, teeth, face_width, cone_angle, shaft_diam, hide_root, anchor, orient', desc: 'Bevel gear' },
35
+ 'rack': { params: 'pitch, teeth, thickness, height, anchor, orient', desc: 'Gear rack' },
36
+ 'threaded_rod': { params: 'diameter, length, pitch, internal, bevel, orient, anchor', desc: 'Threaded rod/bolt' },
37
+ 'threaded_nut': { params: 'diameter, thickness, pitch, internal, bevel, anchor, orient', desc: 'Threaded nut' },
38
+ 'bezier_surface': { params: 'patches, N, style', desc: 'Bezier surface patch' },
39
+ 'bezier_path': { params: 'path, N, closed, uniform', desc: 'Bezier curve path' },
40
+ 'cuboid': { params: 'size, rounding, edges, corners, p1, p2, anchor, orient, spin', desc: 'Rounded cube' },
41
+ 'cyl': { params: 'l, r, r1, r2, anchor, orient, spin', desc: 'Cylinder with chamfer' },
42
+ 'xcyl': { params: 'l, r, anchor, orient, spin', desc: 'X-axis cylinder' },
43
+ 'ycyl': { params: 'l, r, anchor, orient, spin', desc: 'Y-axis cylinder' },
44
+ 'zcyl': { params: 'l, r, anchor, orient, spin', desc: 'Z-axis cylinder' },
45
+ 'prism': { params: 'n, size, height, center, anchor, orient, spin', desc: 'Regular prism' },
46
+ 'hexagon': { params: 'size, height, center, anchor, orient, spin', desc: 'Regular hexagon' },
47
+ 'octagon': { params: 'size, height, center, anchor, orient, spin', desc: 'Regular octagon' },
48
+ 'egg': { params: 'length, width, r1, r2, base_size, center, anchor, orient, spin', desc: 'Egg shape' },
49
+ 'teardrop': { params: 'r, ang, cap_h, center, anchor, orient, spin', desc: 'Teardrop shape' },
50
+ 'cylinder_chamfer': { params: 'r, h, chamf, anchor, orient, spin', desc: 'Cylinder with chamfered edges' },
51
+ 'cylinder_vent': { params: 'r, l, size, style, anchor, orient, spin', desc: 'Cylinder with vent holes' },
52
+ 'corrugated_surface': { params: 'size, waves, amplitude, thickness, anchor, orient', desc: 'Corrugated surface' },
53
+ 'hemisphere': { params: 'r, anchor, orient, spin', desc: 'Half sphere' },
54
+ 'ring': { params: 'r, r_i, h, center, anchor, orient, spin', desc: 'Ring/annulus' },
55
+ 'torus': { params: 'r_maj, r_min, center, anchor, orient, spin', desc: 'Torus' }
56
+ };
57
+
58
+ const MCAD_SHAPES = {
59
+ 'hexagon': { file: 'regular_shapes', params: 'size, height', desc: 'Regular hexagon' },
60
+ 'octagon': { file: 'regular_shapes', params: 'size, height', desc: 'Regular octagon' },
61
+ 'oval': { file: 'regular_shapes', params: 'width, height, depth', desc: 'Oval cylinder' },
62
+ 'spur_gear': { file: 'involute_gears', params: 'pitch, teeth, width, hole_diam', desc: 'Involute spur gear' },
63
+ 'rack': { file: 'involute_gears', params: 'pitch, teeth, width, height', desc: 'Involute gear rack' },
64
+ 'bearing': { file: 'bearing', params: 'model', desc: 'Standard bearing' },
65
+ 'motor_shaft': { file: 'stepper', params: 'model', desc: 'Stepper motor' },
66
+ 'bolt': { file: 'nuts_and_bolts', params: 'diameter, length, pitch', desc: 'Standard bolt' },
67
+ 'nut': { file: 'nuts_and_bolts', params: 'diameter, height', desc: 'Standard nut' },
68
+ 'washer': { file: 'nuts_and_bolts', params: 'hole_diameter, outer_diameter', desc: 'Washer' }
69
+ };
70
+
71
+ // ============================================================================
72
+ // EXPORT: THREE.JS TO SCAD
73
+ // ============================================================================
74
+
75
+ /**
76
+ * Convert Three.js Geometry to OpenSCAD primitive code
77
+ * @param {THREE.BufferGeometry} geometry
78
+ * @param {THREE.Vector3} position
79
+ * @param {THREE.Euler} rotation
80
+ * @param {THREE.Vector3} scale
81
+ * @returns {string}
82
+ */
83
+ function geometryToSCAD(geometry, position, rotation, scale) {
84
+ let scadCode = '';
85
+
86
+ // Determine geometry type and convert
87
+ if (geometry.constructor.name === 'BoxGeometry') {
88
+ const params = geometry.parameters;
89
+ const w = (params.width * (scale?.x || 1)).toFixed(2);
90
+ const h = (params.height * (scale?.y || 1)).toFixed(2);
91
+ const d = (params.depth * (scale?.z || 1)).toFixed(2);
92
+ scadCode = `cube([${w}, ${h}, ${d}], center=true)`;
93
+ }
94
+ else if (geometry.constructor.name === 'CylinderGeometry') {
95
+ const params = geometry.parameters;
96
+ const r1 = (params.radiusTop * (scale?.x || 1)).toFixed(2);
97
+ const r2 = (params.radiusBottom * (scale?.x || 1)).toFixed(2);
98
+ const h = (params.height * (scale?.z || 1)).toFixed(2);
99
+ const fn = params.radialSegments || 32;
100
+ scadCode = `cylinder(h=${h}, r1=${r1}, r2=${r2}, $fn=${fn})`;
101
+ }
102
+ else if (geometry.constructor.name === 'SphereGeometry') {
103
+ const params = geometry.parameters;
104
+ const r = (params.radius * (scale?.x || 1)).toFixed(2);
105
+ const fn = params.widthSegments || 32;
106
+ scadCode = `sphere(r=${r}, $fn=${fn})`;
107
+ }
108
+ else if (geometry.constructor.name === 'ConeGeometry') {
109
+ const params = geometry.parameters;
110
+ const r = (params.radius * (scale?.x || 1)).toFixed(2);
111
+ const h = (params.height * (scale?.z || 1)).toFixed(2);
112
+ const fn = params.radialSegments || 32;
113
+ scadCode = `cylinder(h=${h}, r1=${r}, r2=0, $fn=${fn})`;
114
+ }
115
+ else if (geometry.constructor.name === 'TorusGeometry') {
116
+ const params = geometry.parameters;
117
+ const r_maj = (params.radius * (scale?.x || 1)).toFixed(2);
118
+ const r_min = (params.tube * (scale?.x || 1)).toFixed(2);
119
+ scadCode = `rotate_extrude($fn=64) translate([${r_maj}, 0]) circle(r=${r_min}, $fn=32)`;
120
+ }
121
+ else {
122
+ // Fallback: use ExtrudeGeometry or default cube
123
+ scadCode = `cube([10, 10, 10]) // Unknown geometry type: ${geometry.constructor.name}`;
124
+ }
125
+
126
+ // Apply transforms
127
+ let transformedCode = scadCode;
128
+
129
+ // Scale
130
+ if (scale && (scale.x !== 1 || scale.y !== 1 || scale.z !== 1)) {
131
+ transformedCode = `scale([${scale.x.toFixed(3)}, ${scale.y.toFixed(3)}, ${scale.z.toFixed(3)}]) ${transformedCode}`;
132
+ }
133
+
134
+ // Rotation (convert to degrees)
135
+ if (rotation && (rotation.x !== 0 || rotation.y !== 0 || rotation.z !== 0)) {
136
+ const degX = (rotation.x * 180 / Math.PI).toFixed(2);
137
+ const degY = (rotation.y * 180 / Math.PI).toFixed(2);
138
+ const degZ = (rotation.z * 180 / Math.PI).toFixed(2);
139
+ transformedCode = `rotate([${degX}, ${degY}, ${degZ}]) ${transformedCode}`;
140
+ }
141
+
142
+ // Translation
143
+ if (position && (position.x !== 0 || position.y !== 0 || position.z !== 0)) {
144
+ transformedCode = `translate([${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}]) ${transformedCode}`;
145
+ }
146
+
147
+ return transformedCode;
148
+ }
149
+
150
+ /**
151
+ * Traverse Three.js scene and convert to SCAD code
152
+ * @param {THREE.Scene} sceneGraph
153
+ * @returns {string}
154
+ */
155
+ function traverseScene(sceneGraph) {
156
+ let variables = [];
157
+ let geometry = [];
158
+ let visited = new Set();
159
+
160
+ function traverse(obj, depth = 0) {
161
+ if (visited.has(obj.uuid)) return;
162
+ visited.add(obj.uuid);
163
+
164
+ const indent = ' '.repeat(depth);
165
+
166
+ // Process meshes
167
+ if (obj.isMesh) {
168
+ const meshCode = geometryToSCAD(obj.geometry, obj.position, obj.rotation, obj.scale);
169
+ const name = obj.name || `mesh_${geometry.length}`;
170
+ geometry.push(`${indent}// ${name}\n${indent}${meshCode}`);
171
+ }
172
+
173
+ // Process groups (as modules)
174
+ if (obj.children.length > 0 && obj.isGroup) {
175
+ const groupName = obj.name || `group_${geometry.length}`;
176
+ geometry.push(`\n${indent}// Group: ${groupName}`);
177
+ }
178
+
179
+ // Recurse
180
+ obj.children.forEach(child => traverse(child, depth + 1));
181
+ }
182
+
183
+ traverse(sceneGraph);
184
+
185
+ // Build complete SCAD code
186
+ let scadCode = '';
187
+ scadCode += '// Generated by cycleCAD OpenSCAD Export\n';
188
+ scadCode += '// Use <BOSL2/std.scad> // Uncomment for BOSL2 library\n';
189
+ scadCode += '\n';
190
+ scadCode += '// ============= PARAMETERS =============\n';
191
+ scadCode += '$fn = 64; // Resolution: higher = smoother, slower\n';
192
+ scadCode += '\n';
193
+ scadCode += '// ============= GEOMETRY =============\n';
194
+ scadCode += geometry.join('\n');
195
+ scadCode += '\n';
196
+
197
+ return scadCode;
198
+ }
199
+
200
+ /**
201
+ * Export current scene to SCAD
202
+ * @returns {string}
203
+ */
204
+ function exportScene() {
205
+ if (!scene) {
206
+ console.error('Scene not initialized');
207
+ return '';
208
+ }
209
+ return traverseScene(scene);
210
+ }
211
+
212
+ /**
213
+ * Export single mesh to SCAD
214
+ * @param {THREE.Mesh} mesh
215
+ * @returns {string}
216
+ */
217
+ function exportMesh(mesh) {
218
+ return geometryToSCAD(mesh.geometry, mesh.position, mesh.rotation, mesh.scale);
219
+ }
220
+
221
+ // ============================================================================
222
+ // IMPORT: SCAD TO THREE.JS
223
+ // ============================================================================
224
+
225
+ /**
226
+ * Simple SCAD parser - builds AST from SCAD text
227
+ * @param {string} scadCode
228
+ * @returns {object}
229
+ */
230
+ function parseSCAD(scadCode) {
231
+ const ast = {
232
+ includes: [],
233
+ uses: [],
234
+ variables: {},
235
+ functions: {},
236
+ modules: {},
237
+ objects: []
238
+ };
239
+
240
+ // Remove comments
241
+ scadCode = scadCode.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
242
+
243
+ // Extract includes
244
+ const includeMatches = scadCode.match(/include\s*<([^>]+)>/g) || [];
245
+ ast.includes = includeMatches.map(m => m.match(/<([^>]+)>/)[1]);
246
+
247
+ // Extract uses
248
+ const useMatches = scadCode.match(/use\s*<([^>]+)>/g) || [];
249
+ ast.uses = useMatches.map(m => m.match(/<([^>]+)>/)[1]);
250
+
251
+ // Extract variables: name = value;
252
+ const varMatches = scadCode.match(/(\w+)\s*=\s*([^;]+);/g) || [];
253
+ varMatches.forEach(match => {
254
+ const parts = match.match(/(\w+)\s*=\s*([^;]+);/);
255
+ if (parts) {
256
+ ast.variables[parts[1]] = parts[2].trim();
257
+ }
258
+ });
259
+
260
+ return ast;
261
+ }
262
+
263
+ /**
264
+ * Evaluate simple SCAD expressions
265
+ * @param {string} expr
266
+ * @param {object} context - variables context
267
+ * @returns {*}
268
+ */
269
+ function evaluateSCADExpr(expr, context = {}) {
270
+ try {
271
+ // Replace variable references
272
+ let code = expr;
273
+ Object.keys(context).forEach(key => {
274
+ code = code.replace(new RegExp(`\\b${key}\\b`, 'g'), `(${context[key]})`);
275
+ });
276
+ // Evaluate math safely
277
+ return Function(`"use strict"; return (${code})`)();
278
+ } catch (e) {
279
+ console.warn(`Failed to evaluate SCAD expression: ${expr}`, e);
280
+ return null;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Convert SCAD code to Three.js geometry
286
+ * @param {string} scadCode
287
+ * @returns {THREE.Group}
288
+ */
289
+ function importSCAD(scadCode) {
290
+ const ast = parseSCAD(scadCode);
291
+ const group = new THREE.Group();
292
+ const context = ast.variables;
293
+
294
+ // Parse and create geometry
295
+ const lines = scadCode.split('\n');
296
+
297
+ lines.forEach(line => {
298
+ line = line.trim();
299
+
300
+ // Skip empty lines and comments
301
+ if (!line || line.startsWith('//') || line.startsWith('/*')) return;
302
+
303
+ // cube([w, h, d])
304
+ if (line.includes('cube(')) {
305
+ const match = line.match(/cube\(\[([^\]]+)\]/);
306
+ if (match) {
307
+ const dims = match[1].split(',').map(d => evaluateSCADExpr(d.trim(), context));
308
+ const geo = new THREE.BoxGeometry(dims[0], dims[1], dims[2]);
309
+ group.add(new THREE.Mesh(geo));
310
+ }
311
+ }
312
+
313
+ // cylinder(h=..., r=...)
314
+ if (line.includes('cylinder(')) {
315
+ const hMatch = line.match(/h=([^,)]+)/);
316
+ const rMatch = line.match(/r=([^,)]+)/);
317
+ const h = hMatch ? evaluateSCADExpr(hMatch[1].trim(), context) : 10;
318
+ const r = rMatch ? evaluateSCADExpr(rMatch[1].trim(), context) : 5;
319
+ const geo = new THREE.CylinderGeometry(r, r, h, 32);
320
+ group.add(new THREE.Mesh(geo));
321
+ }
322
+
323
+ // sphere(r=...)
324
+ if (line.includes('sphere(')) {
325
+ const match = line.match(/r=([^,)]+)/);
326
+ const r = match ? evaluateSCADExpr(match[1].trim(), context) : 5;
327
+ const geo = new THREE.SphereGeometry(r, 32, 32);
328
+ group.add(new THREE.Mesh(geo));
329
+ }
330
+
331
+ // translate([x, y, z])
332
+ if (line.includes('translate(')) {
333
+ const match = line.match(/translate\(\[([^\]]+)\]/);
334
+ if (match) {
335
+ const coords = match[1].split(',').map(d => evaluateSCADExpr(d.trim(), context));
336
+ // Parse child object on same line (simplified)
337
+ const child = group.children[group.children.length - 1];
338
+ if (child) {
339
+ child.position.set(coords[0], coords[1], coords[2]);
340
+ }
341
+ }
342
+ }
343
+ });
344
+
345
+ return group;
346
+ }
347
+
348
+ // ============================================================================
349
+ // CODE FORMATTING
350
+ // ============================================================================
351
+
352
+ /**
353
+ * Pretty-print SCAD code
354
+ * @param {string} code
355
+ * @returns {string}
356
+ */
357
+ function formatCode(code) {
358
+ let formatted = '';
359
+ let indentLevel = 0;
360
+ let inString = false;
361
+ let stringChar = '';
362
+
363
+ for (let i = 0; i < code.length; i++) {
364
+ const char = code[i];
365
+ const prevChar = i > 0 ? code[i - 1] : '';
366
+
367
+ // Track strings
368
+ if ((char === '"' || char === "'") && prevChar !== '\\') {
369
+ inString = !inString;
370
+ stringChar = inString ? char : '';
371
+ }
372
+
373
+ if (!inString) {
374
+ if (char === '{' || char === '[' || char === '(') {
375
+ formatted += char;
376
+ if (char === '{' || char === '[') {
377
+ indentLevel++;
378
+ formatted += '\n' + ' '.repeat(indentLevel);
379
+ }
380
+ } else if (char === '}' || char === ']') {
381
+ indentLevel = Math.max(0, indentLevel - 1);
382
+ formatted = formatted.trimEnd() + '\n' + ' '.repeat(indentLevel) + char;
383
+ } else if (char === ';') {
384
+ formatted += char + '\n' + ' '.repeat(indentLevel);
385
+ } else if (char === ',' && prevChar !== ' ') {
386
+ formatted += char + ' ';
387
+ } else if (char === ' ' && prevChar === ' ') {
388
+ // Skip duplicate spaces
389
+ } else {
390
+ formatted += char;
391
+ }
392
+ } else {
393
+ formatted += char;
394
+ }
395
+ }
396
+
397
+ return formatted.trim();
398
+ }
399
+
400
+ // ============================================================================
401
+ // UI & INTEGRATION
402
+ // ============================================================================
403
+
404
+ /**
405
+ * Get UI panel HTML
406
+ * @returns {string}
407
+ */
408
+ function getUI() {
409
+ return `
410
+ <div id="scad-export-panel" style="padding: 12px;">
411
+ <h3 style="margin: 0 0 12px 0; font-size: 14px; font-weight: bold;">OpenSCAD Export</h3>
412
+
413
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px;">
414
+ <button class="scad-btn" data-action="scad-export-scene"
415
+ style="padding: 8px; background: #0284C7; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
416
+ Export Scene
417
+ </button>
418
+ <button class="scad-btn" data-action="scad-export-selected"
419
+ style="padding: 8px; background: #0284C7; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
420
+ Export Selected
421
+ </button>
422
+ </div>
423
+
424
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px;">
425
+ <button class="scad-btn" data-action="scad-import"
426
+ style="padding: 8px; background: #7C3AED; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
427
+ Import SCAD
428
+ </button>
429
+ <button class="scad-btn" data-action="scad-library"
430
+ style="padding: 8px; background: #059669; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
431
+ BOSL2 Library
432
+ </button>
433
+ </div>
434
+
435
+ <div style="margin-bottom: 12px;">
436
+ <label style="display: block; font-size: 11px; margin-bottom: 4px; color: #666;">
437
+ <input type="checkbox" id="scad-use-bosl2" checked /> Use BOSL2 library
438
+ </label>
439
+ <label style="display: block; font-size: 11px; color: #666;">
440
+ <input type="checkbox" id="scad-use-mcad" /> Use MCAD library
441
+ </label>
442
+ </div>
443
+
444
+ <div style="margin-bottom: 12px;">
445
+ <label style="display: block; font-size: 11px; margin-bottom: 4px; color: #666;">
446
+ Render Quality ($fn):
447
+ </label>
448
+ <input type="range" id="scad-quality" min="8" max="128" value="64"
449
+ style="width: 100%; cursor: pointer;" />
450
+ <span id="scad-quality-val" style="font-size: 11px; color: #888;">64</span>
451
+ </div>
452
+
453
+ <textarea id="scad-code" placeholder="SCAD code will appear here..."
454
+ style="width: 100%; height: 200px; padding: 8px; border: 1px solid #ddd; border-radius: 4px;
455
+ font-family: 'Courier New', monospace; font-size: 11px; resize: vertical; margin-bottom: 8px;">
456
+ </textarea>
457
+
458
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
459
+ <button class="scad-btn" data-action="scad-copy"
460
+ style="padding: 8px; background: #1F2937; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
461
+ Copy to Clipboard
462
+ </button>
463
+ <button class="scad-btn" data-action="scad-download"
464
+ style="padding: 8px; background: #1F2937; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
465
+ Download .scad
466
+ </button>
467
+ </div>
468
+ </div>
469
+ `;
470
+ }
471
+
472
+ /**
473
+ * Get library shapes list
474
+ * @returns {object}
475
+ */
476
+ function getLibraryShapes() {
477
+ return {
478
+ BOSL2: BOSL2_SHAPES,
479
+ MCAD: MCAD_SHAPES,
480
+ count: Object.keys(BOSL2_SHAPES).length + Object.keys(MCAD_SHAPES).length
481
+ };
482
+ }
483
+
484
+ /**
485
+ * Initialize module
486
+ * @param {THREE.Scene} gameScene
487
+ * @param {THREE.Camera} gameCamera
488
+ */
489
+ function init(gameScene, gameCamera) {
490
+ scene = gameScene;
491
+ camera = gameCamera;
492
+
493
+ // Attach event listeners to UI buttons
494
+ document.addEventListener('click', (e) => {
495
+ const btn = e.target.closest('.scad-btn');
496
+ if (!btn) return;
497
+
498
+ const action = btn.getAttribute('data-action');
499
+ execute(action);
500
+ });
501
+
502
+ // Quality slider update
503
+ const qualitySlider = document.getElementById('scad-quality');
504
+ if (qualitySlider) {
505
+ qualitySlider.addEventListener('change', (e) => {
506
+ document.getElementById('scad-quality-val').textContent = e.target.value;
507
+ });
508
+ }
509
+
510
+ console.log('SCAD Export module initialized');
511
+ }
512
+
513
+ /**
514
+ * Execute action
515
+ * @param {string} action
516
+ * @param {object} params
517
+ */
518
+ function execute(action, params = {}) {
519
+ const codeBox = document.getElementById('scad-code');
520
+ const useBOSL2 = document.getElementById('scad-use-bosl2')?.checked;
521
+ const useMCAD = document.getElementById('scad-use-mcad')?.checked;
522
+ const quality = document.getElementById('scad-quality')?.value || 64;
523
+
524
+ switch (action) {
525
+ case 'scad-export-scene': {
526
+ let code = exportScene();
527
+
528
+ // Add library includes
529
+ if (useBOSL2) {
530
+ code = `include <BOSL2/std.scad>\n\n${code}`;
531
+ }
532
+ if (useMCAD) {
533
+ code = `use <MCAD/regular_shapes.scad>\nuse <MCAD/involute_gears.scad>\n\n${code}`;
534
+ }
535
+
536
+ // Format and show
537
+ code = formatCode(code);
538
+ code = code.replace('$fn = 64;', `$fn = ${quality};`);
539
+ if (codeBox) codeBox.value = code;
540
+ console.log('Scene exported to SCAD');
541
+ break;
542
+ }
543
+
544
+ case 'scad-export-selected': {
545
+ // Export selected object (simplified - exports first selected)
546
+ const selectedObj = window.selectedMesh || (scene?.children[0]);
547
+ if (!selectedObj || !selectedObj.isMesh) {
548
+ alert('No mesh selected');
549
+ return;
550
+ }
551
+ const code = exportMesh(selectedObj);
552
+ if (codeBox) codeBox.value = code;
553
+ console.log('Selected mesh exported to SCAD');
554
+ break;
555
+ }
556
+
557
+ case 'scad-import': {
558
+ const code = codeBox?.value || '';
559
+ if (!code.trim()) {
560
+ alert('No SCAD code to import');
561
+ return;
562
+ }
563
+ const group = importSCAD(code);
564
+ scene?.add(group);
565
+ console.log('SCAD imported to scene');
566
+ break;
567
+ }
568
+
569
+ case 'scad-library': {
570
+ const libs = getLibraryShapes();
571
+ const msg = `BOSL2: ${Object.keys(libs.BOSL2).length} shapes\nMCAD: ${Object.keys(libs.MCAD).length} shapes\n\nSee console for full list`;
572
+ alert(msg);
573
+ console.table(libs.BOSL2);
574
+ console.table(libs.MCAD);
575
+ break;
576
+ }
577
+
578
+ case 'scad-copy': {
579
+ const code = codeBox?.value || '';
580
+ if (!code.trim()) {
581
+ alert('No code to copy');
582
+ return;
583
+ }
584
+ navigator.clipboard.writeText(code).then(() => {
585
+ alert('Copied to clipboard!');
586
+ });
587
+ break;
588
+ }
589
+
590
+ case 'scad-download': {
591
+ const code = codeBox?.value || '';
592
+ if (!code.trim()) {
593
+ alert('No code to download');
594
+ return;
595
+ }
596
+ const blob = new Blob([code], { type: 'text/plain' });
597
+ const url = URL.createObjectURL(blob);
598
+ const a = document.createElement('a');
599
+ a.href = url;
600
+ a.download = 'design.scad';
601
+ a.click();
602
+ URL.revokeObjectURL(url);
603
+ console.log('SCAD file downloaded');
604
+ break;
605
+ }
606
+
607
+ default:
608
+ console.warn(`Unknown SCAD action: ${action}`);
609
+ }
610
+ }
611
+
612
+ // ============================================================================
613
+ // PUBLIC API
614
+ // ============================================================================
615
+
616
+ return {
617
+ init,
618
+ getUI,
619
+ execute,
620
+ exportScene,
621
+ exportMesh,
622
+ importSCAD,
623
+ formatCode,
624
+ getLibraryShapes,
625
+ geometryToSCAD,
626
+ parseSCAD,
627
+ evaluateSCADExpr,
628
+ // Exports for testing/advanced use
629
+ BOSL2_SHAPES,
630
+ MCAD_SHAPES
631
+ };
632
+ })();
633
+
634
+ // Register on global
635
+ if (typeof window !== 'undefined') {
636
+ if (!window.CycleCAD) window.CycleCAD = {};
637
+ window.CycleCAD.SCADExport = CycleCADSCADExport;
638
+ }
639
+
640
+ // Export for Node.js/module systems
641
+ if (typeof module !== 'undefined' && module.exports) {
642
+ module.exports = CycleCADSCADExport;
643
+ }