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.
- package/app/index.html +558 -25
- package/app/js/explodeview-full.js +1141 -0
- package/app/js/modules/image-to-cad.js +1184 -0
- package/app/js/modules/openscad-engine.js +817 -0
- package/app/js/modules/parametric-sliders.js +1322 -0
- package/app/js/modules/scad-export.js +643 -0
- package/app/js/test-compat-shim.js +121 -0
- package/app/tests/killer-features-visual-test.html +71 -49
- package/package.json +1 -1
|
@@ -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
|
+
}
|