cyclecad 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,658 @@
1
+ /**
2
+ * export.js - cycleCAD Export Module
3
+ * Handles exporting 3D models in various formats (STL, OBJ, glTF, STEP, JSON)
4
+ * and importing cycleCAD native JSON format
5
+ */
6
+
7
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
8
+
9
+ /**
10
+ * Download a file to the user's computer
11
+ * @param {string|ArrayBuffer} content - File content
12
+ * @param {string} filename - Filename for download
13
+ * @param {string} mimeType - MIME type (e.g., 'text/plain', 'application/octet-stream')
14
+ */
15
+ export function downloadFile(content, filename, mimeType) {
16
+ let blob;
17
+ if (content instanceof ArrayBuffer) {
18
+ blob = new Blob([content], { type: mimeType });
19
+ } else {
20
+ blob = new Blob([content], { type: mimeType });
21
+ }
22
+
23
+ const url = URL.createObjectURL(blob);
24
+ const link = document.createElement('a');
25
+ link.href = url;
26
+ link.download = filename;
27
+ document.body.appendChild(link);
28
+ link.click();
29
+ document.body.removeChild(link);
30
+ URL.revokeObjectURL(url);
31
+ }
32
+
33
+ /**
34
+ * Extract triangles from a Three.js BufferGeometry
35
+ * Handles both indexed and non-indexed geometries
36
+ * @param {THREE.BufferGeometry} geometry - The geometry to extract from
37
+ * @returns {Array<{vertices: number[][], normal: number[]}>} Array of triangles
38
+ */
39
+ function extractTrianglesFromGeometry(geometry) {
40
+ const triangles = [];
41
+ const positions = geometry.getAttribute('position');
42
+ const normals = geometry.getAttribute('normal');
43
+ const indices = geometry.getIndex();
44
+
45
+ if (!positions) return triangles;
46
+
47
+ const posArray = positions.array;
48
+ const normArray = normals ? normals.array : null;
49
+ const indexArray = indices ? indices.array : null;
50
+
51
+ let triangleCount;
52
+ if (indexArray) {
53
+ triangleCount = indexArray.length / 3;
54
+ } else {
55
+ triangleCount = posArray.length / 9; // 3 vertices * 3 coords per triangle
56
+ }
57
+
58
+ for (let i = 0; i < triangleCount; i++) {
59
+ let idx0, idx1, idx2;
60
+
61
+ if (indexArray) {
62
+ idx0 = indexArray[i * 3] * 3;
63
+ idx1 = indexArray[i * 3 + 1] * 3;
64
+ idx2 = indexArray[i * 3 + 2] * 3;
65
+ } else {
66
+ idx0 = i * 9;
67
+ idx1 = i * 9 + 3;
68
+ idx2 = i * 9 + 6;
69
+ }
70
+
71
+ const v0 = [posArray[idx0], posArray[idx0 + 1], posArray[idx0 + 2]];
72
+ const v1 = [posArray[idx1], posArray[idx1 + 1], posArray[idx1 + 2]];
73
+ const v2 = [posArray[idx2], posArray[idx2 + 1], posArray[idx2 + 2]];
74
+
75
+ let normal = [0, 0, 1];
76
+ if (normArray) {
77
+ const nIdx = (indexArray ? indexArray[i * 3] : i * 3) * 3;
78
+ normal = [normArray[nIdx], normArray[nIdx + 1], normArray[nIdx + 2]];
79
+ } else {
80
+ // Calculate normal from vertices if not provided
81
+ const edge1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
82
+ const edge2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
83
+ normal = crossProduct(edge1, edge2);
84
+ normal = normalizeVector(normal);
85
+ }
86
+
87
+ triangles.push({
88
+ vertices: [v0, v1, v2],
89
+ normal: normal
90
+ });
91
+ }
92
+
93
+ return triangles;
94
+ }
95
+
96
+ /**
97
+ * Calculate cross product of two 3D vectors
98
+ * @param {number[]} a - Vector A
99
+ * @param {number[]} b - Vector B
100
+ * @returns {number[]} Cross product
101
+ */
102
+ function crossProduct(a, b) {
103
+ return [
104
+ a[1] * b[2] - a[2] * b[1],
105
+ a[2] * b[0] - a[0] * b[2],
106
+ a[0] * b[1] - a[1] * b[0]
107
+ ];
108
+ }
109
+
110
+ /**
111
+ * Normalize a 3D vector
112
+ * @param {number[]} v - Vector to normalize
113
+ * @returns {number[]} Normalized vector
114
+ */
115
+ function normalizeVector(v) {
116
+ const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
117
+ if (length === 0) return [0, 0, 1];
118
+ return [v[0] / length, v[1] / length, v[2] / length];
119
+ }
120
+
121
+ /**
122
+ * Transform a 3D point by a 4x4 matrix
123
+ * @param {number[]} point - Point [x, y, z]
124
+ * @param {THREE.Matrix4} matrix - Transform matrix
125
+ * @returns {number[]} Transformed point
126
+ */
127
+ function transformPoint(point, matrix) {
128
+ const v = new THREE.Vector3(point[0], point[1], point[2]);
129
+ v.applyMatrix4(matrix);
130
+ return [v.x, v.y, v.z];
131
+ }
132
+
133
+ /**
134
+ * Export features as ASCII STL format
135
+ * @param {Array} features - Array of feature objects with mesh property
136
+ * @param {string} filename - Output filename
137
+ */
138
+ export function exportSTL(features, filename = 'model.stl') {
139
+ let stlContent = 'solid cycleCAD_Model\n';
140
+
141
+ features.forEach((feature, fIdx) => {
142
+ if (!feature.mesh || !feature.mesh.geometry) return;
143
+
144
+ const geometry = feature.mesh.geometry.clone();
145
+ geometry.computeVertexNormals();
146
+
147
+ const triangles = extractTrianglesFromGeometry(geometry);
148
+ const worldMatrix = feature.mesh.matrixWorld;
149
+
150
+ triangles.forEach(triangle => {
151
+ const v0 = transformPoint(triangle.vertices[0], worldMatrix);
152
+ const v1 = transformPoint(triangle.vertices[1], worldMatrix);
153
+ const v2 = transformPoint(triangle.vertices[2], worldMatrix);
154
+
155
+ // Recalculate normal from world-space vertices
156
+ const edge1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
157
+ const edge2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
158
+ const normal = normalizeVector(crossProduct(edge1, edge2));
159
+
160
+ stlContent += ` facet normal ${normal[0].toFixed(8)} ${normal[1].toFixed(8)} ${normal[2].toFixed(8)}\n`;
161
+ stlContent += ` outer loop\n`;
162
+ stlContent += ` vertex ${v0[0].toFixed(8)} ${v0[1].toFixed(8)} ${v0[2].toFixed(8)}\n`;
163
+ stlContent += ` vertex ${v1[0].toFixed(8)} ${v1[1].toFixed(8)} ${v1[2].toFixed(8)}\n`;
164
+ stlContent += ` vertex ${v2[0].toFixed(8)} ${v2[1].toFixed(8)} ${v2[2].toFixed(8)}\n`;
165
+ stlContent += ` endloop\n`;
166
+ stlContent += ` endfacet\n`;
167
+ });
168
+ });
169
+
170
+ stlContent += 'endsolid cycleCAD_Model\n';
171
+ downloadFile(stlContent, filename, 'text/plain');
172
+ }
173
+
174
+ /**
175
+ * Export features as Binary STL format (more compact)
176
+ * @param {Array} features - Array of feature objects with mesh property
177
+ * @param {string} filename - Output filename
178
+ */
179
+ export function exportSTLBinary(features, filename = 'model.stl') {
180
+ const triangles = [];
181
+
182
+ features.forEach((feature, fIdx) => {
183
+ if (!feature.mesh || !feature.mesh.geometry) return;
184
+
185
+ const geometry = feature.mesh.geometry.clone();
186
+ geometry.computeVertexNormals();
187
+
188
+ const tris = extractTrianglesFromGeometry(geometry);
189
+ const worldMatrix = feature.mesh.matrixWorld;
190
+
191
+ tris.forEach(triangle => {
192
+ const v0 = transformPoint(triangle.vertices[0], worldMatrix);
193
+ const v1 = transformPoint(triangle.vertices[1], worldMatrix);
194
+ const v2 = transformPoint(triangle.vertices[2], worldMatrix);
195
+
196
+ const edge1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
197
+ const edge2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
198
+ const normal = normalizeVector(crossProduct(edge1, edge2));
199
+
200
+ triangles.push({
201
+ normal: normal,
202
+ vertices: [v0, v1, v2]
203
+ });
204
+ });
205
+ });
206
+
207
+ // Create binary buffer
208
+ // 80-byte header + 4-byte triangle count + (50 bytes per triangle)
209
+ const buffer = new ArrayBuffer(80 + 4 + triangles.length * 50);
210
+ const view = new DataView(buffer);
211
+
212
+ // Header (80 bytes of zeros, can be used for description)
213
+ const headerText = 'cycleCAD STL Binary Export'.padEnd(80, '\0');
214
+ for (let i = 0; i < 80; i++) {
215
+ view.setUint8(i, headerText.charCodeAt(i));
216
+ }
217
+
218
+ // Triangle count (4 bytes, little-endian)
219
+ view.setUint32(80, triangles.length, true);
220
+
221
+ // Triangle data (50 bytes each)
222
+ let offset = 84;
223
+ triangles.forEach(tri => {
224
+ // Normal (3 × float32)
225
+ view.setFloat32(offset, tri.normal[0], true);
226
+ view.setFloat32(offset + 4, tri.normal[1], true);
227
+ view.setFloat32(offset + 8, tri.normal[2], true);
228
+ offset += 12;
229
+
230
+ // Vertices (3 vertices × 3 coords × float32)
231
+ tri.vertices.forEach(vertex => {
232
+ view.setFloat32(offset, vertex[0], true);
233
+ view.setFloat32(offset + 4, vertex[1], true);
234
+ view.setFloat32(offset + 8, vertex[2], true);
235
+ offset += 12;
236
+ });
237
+
238
+ // Attribute byte count (2 bytes, usually 0)
239
+ view.setUint16(offset, 0, true);
240
+ offset += 2;
241
+ });
242
+
243
+ downloadFile(buffer, filename, 'application/octet-stream');
244
+ }
245
+
246
+ /**
247
+ * Export features as Wavefront OBJ format
248
+ * @param {Array} features - Array of feature objects with mesh property
249
+ * @param {string} filename - Output filename
250
+ */
251
+ export function exportOBJ(features, filename = 'model.obj') {
252
+ let objContent = '# cycleCAD OBJ Export\n';
253
+ objContent += '# https://github.com/vvlars-cmd/cyclecad\n\n';
254
+
255
+ let vertexOffset = 0;
256
+ let normalOffset = 0;
257
+
258
+ features.forEach((feature, fIdx) => {
259
+ if (!feature.mesh || !feature.mesh.geometry) return;
260
+
261
+ const geometry = feature.mesh.geometry.clone();
262
+ geometry.computeVertexNormals();
263
+
264
+ const positions = geometry.getAttribute('position');
265
+ const normals = geometry.getAttribute('normal');
266
+ const indices = geometry.getIndex();
267
+
268
+ if (!positions) return;
269
+
270
+ objContent += `g feature_${fIdx}\n`;
271
+ objContent += `usemtl material_${fIdx}\n`;
272
+
273
+ const posArray = positions.array;
274
+ const normArray = normals ? normals.array : null;
275
+ const worldMatrix = feature.mesh.matrixWorld;
276
+
277
+ // Write vertices
278
+ const vertexCount = posArray.length / 3;
279
+ for (let i = 0; i < vertexCount; i++) {
280
+ const v = transformPoint(
281
+ [posArray[i * 3], posArray[i * 3 + 1], posArray[i * 3 + 2]],
282
+ worldMatrix
283
+ );
284
+ objContent += `v ${v[0].toFixed(8)} ${v[1].toFixed(8)} ${v[2].toFixed(8)}\n`;
285
+ }
286
+
287
+ // Write normals
288
+ if (normArray) {
289
+ const normCount = normArray.length / 3;
290
+ for (let i = 0; i < normCount; i++) {
291
+ objContent += `vn ${normArray[i * 3].toFixed(8)} ${normArray[i * 3 + 1].toFixed(8)} ${normArray[i * 3 + 2].toFixed(8)}\n`;
292
+ }
293
+ }
294
+
295
+ // Write faces
296
+ const indexArray = indices ? indices.array : null;
297
+ let triangleCount;
298
+ if (indexArray) {
299
+ triangleCount = indexArray.length / 3;
300
+ } else {
301
+ triangleCount = posArray.length / 9;
302
+ }
303
+
304
+ for (let i = 0; i < triangleCount; i++) {
305
+ let idx0, idx1, idx2;
306
+
307
+ if (indexArray) {
308
+ idx0 = indexArray[i * 3] + 1 + vertexOffset; // OBJ uses 1-based indexing
309
+ idx1 = indexArray[i * 3 + 1] + 1 + vertexOffset;
310
+ idx2 = indexArray[i * 3 + 2] + 1 + vertexOffset;
311
+ } else {
312
+ idx0 = i * 3 + 1 + vertexOffset;
313
+ idx1 = i * 3 + 2 + vertexOffset;
314
+ idx2 = i * 3 + 3 + vertexOffset;
315
+ }
316
+
317
+ if (normArray) {
318
+ const nIdx0 = idx0 + normalOffset - 1;
319
+ const nIdx1 = idx1 + normalOffset - 1;
320
+ const nIdx2 = idx2 + normalOffset - 1;
321
+ objContent += `f ${idx0}/${idx0}/${nIdx0} ${idx1}/${idx1}/${nIdx1} ${idx2}/${idx2}/${nIdx2}\n`;
322
+ } else {
323
+ objContent += `f ${idx0} ${idx1} ${idx2}\n`;
324
+ }
325
+ }
326
+
327
+ vertexOffset += vertexCount;
328
+ if (normArray) normalOffset += normArray.length / 3;
329
+ });
330
+
331
+ // Append MTL reference
332
+ objContent += '\n# Material definitions\n';
333
+ objContent += '# Uncomment below or create separate .mtl file\n';
334
+ features.forEach((feature, fIdx) => {
335
+ objContent += `# newmtl material_${fIdx}\n`;
336
+ objContent += `# Kd 0.8 0.8 0.8\n`;
337
+ });
338
+
339
+ downloadFile(objContent, filename, 'text/plain');
340
+ }
341
+
342
+ /**
343
+ * Export features as glTF 2.0 JSON format
344
+ * @param {Array} features - Array of feature objects with mesh property
345
+ * @param {string} filename - Output filename (usually .gltf)
346
+ */
347
+ export function exportGLTF(features, filename = 'model.gltf') {
348
+ const gltf = {
349
+ asset: {
350
+ version: '2.0',
351
+ generator: 'cycleCAD v0.1'
352
+ },
353
+ scene: 0,
354
+ scenes: [{ nodes: [] }],
355
+ nodes: [],
356
+ meshes: [],
357
+ geometries: [],
358
+ materials: [],
359
+ accessors: [],
360
+ bufferViews: [],
361
+ buffers: []
362
+ };
363
+
364
+ let bufferData = [];
365
+ let bufferViewIndex = 0;
366
+ let accessorIndex = 0;
367
+ let nodeIndex = 0;
368
+
369
+ features.forEach((feature, fIdx) => {
370
+ if (!feature.mesh || !feature.mesh.geometry) return;
371
+
372
+ const geometry = feature.mesh.geometry.clone();
373
+ geometry.computeVertexNormals();
374
+
375
+ const positions = geometry.getAttribute('position');
376
+ const normals = geometry.getAttribute('normal');
377
+ const indices = geometry.getIndex();
378
+
379
+ if (!positions) return;
380
+
381
+ const posArray = positions.array;
382
+ const normArray = normals ? normals.array : null;
383
+ const indexArray = indices ? indices.array : null;
384
+ const worldMatrix = feature.mesh.matrixWorld;
385
+
386
+ // Apply world matrix to positions
387
+ const transformedPos = [];
388
+ const vertexCount = posArray.length / 3;
389
+ for (let i = 0; i < vertexCount; i++) {
390
+ const v = transformPoint(
391
+ [posArray[i * 3], posArray[i * 3 + 1], posArray[i * 3 + 2]],
392
+ worldMatrix
393
+ );
394
+ transformedPos.push(...v);
395
+ }
396
+
397
+ // Create position accessor
398
+ const posAccessor = {
399
+ bufferView: bufferViewIndex,
400
+ componentType: 5126, // FLOAT
401
+ count: vertexCount,
402
+ type: 'VEC3',
403
+ min: [Math.min(...transformedPos.filter((_, i) => i % 3 === 0)),
404
+ Math.min(...transformedPos.filter((_, i) => i % 3 === 1)),
405
+ Math.min(...transformedPos.filter((_, i) => i % 3 === 2))],
406
+ max: [Math.max(...transformedPos.filter((_, i) => i % 3 === 0)),
407
+ Math.max(...transformedPos.filter((_, i) => i % 3 === 1)),
408
+ Math.max(...transformedPos.filter((_, i) => i % 3 === 2))]
409
+ };
410
+
411
+ const posAccessorIndex = accessorIndex++;
412
+ gltf.accessors.push(posAccessor);
413
+
414
+ // Add position data to buffer
415
+ const posBuffer = new Float32Array(transformedPos);
416
+ bufferData.push(posBuffer);
417
+
418
+ gltf.bufferViews.push({
419
+ buffer: 0,
420
+ byteOffset: bufferData.reduce((sum, buf) => sum + buf.byteLength, 0) - posBuffer.byteLength,
421
+ byteLength: posBuffer.byteLength,
422
+ target: 34962 // ARRAY_BUFFER
423
+ });
424
+ bufferViewIndex++;
425
+
426
+ // Create normal accessor if available
427
+ let normalAccessorIndex = -1;
428
+ if (normArray) {
429
+ const normAccessor = {
430
+ bufferView: bufferViewIndex,
431
+ componentType: 5126,
432
+ count: vertexCount,
433
+ type: 'VEC3'
434
+ };
435
+ normalAccessorIndex = accessorIndex++;
436
+ gltf.accessors.push(normAccessor);
437
+
438
+ const normBuffer = new Float32Array(normArray);
439
+ bufferData.push(normBuffer);
440
+
441
+ gltf.bufferViews.push({
442
+ buffer: 0,
443
+ byteOffset: bufferData.reduce((sum, buf) => sum + buf.byteLength, 0) - normBuffer.byteLength,
444
+ byteLength: normBuffer.byteLength,
445
+ target: 34962
446
+ });
447
+ bufferViewIndex++;
448
+ }
449
+
450
+ // Create index accessor if available
451
+ let indicesAccessorIndex = -1;
452
+ if (indexArray) {
453
+ const indAccessor = {
454
+ bufferView: bufferViewIndex,
455
+ componentType: 5125, // UNSIGNED_INT
456
+ count: indexArray.length,
457
+ type: 'SCALAR'
458
+ };
459
+ indicesAccessorIndex = accessorIndex++;
460
+ gltf.accessors.push(indAccessor);
461
+
462
+ const indBuffer = new Uint32Array(indexArray);
463
+ bufferData.push(indBuffer);
464
+
465
+ gltf.bufferViews.push({
466
+ buffer: 0,
467
+ byteOffset: bufferData.reduce((sum, buf) => sum + buf.byteLength, 0) - indBuffer.byteLength,
468
+ byteLength: indBuffer.byteLength,
469
+ target: 34963 // ELEMENT_ARRAY_BUFFER
470
+ });
471
+ bufferViewIndex++;
472
+ }
473
+
474
+ // Create primitive
475
+ const primitive = {
476
+ attributes: {
477
+ POSITION: posAccessorIndex
478
+ },
479
+ mode: 4 // TRIANGLES
480
+ };
481
+
482
+ if (normalAccessorIndex >= 0) {
483
+ primitive.attributes.NORMAL = normalAccessorIndex;
484
+ }
485
+
486
+ if (indicesAccessorIndex >= 0) {
487
+ primitive.indices = indicesAccessorIndex;
488
+ }
489
+
490
+ // Create material
491
+ const materialIndex = gltf.materials.length;
492
+ gltf.materials.push({
493
+ pbrMetallicRoughness: {
494
+ baseColorFactor: [0.8, 0.8, 0.8, 1.0],
495
+ metallicFactor: 0.0,
496
+ roughnessFactor: 1.0
497
+ }
498
+ });
499
+ primitive.material = materialIndex;
500
+
501
+ // Create mesh
502
+ const meshIndex = gltf.meshes.length;
503
+ gltf.meshes.push({
504
+ primitives: [primitive]
505
+ });
506
+
507
+ // Create node
508
+ const nodeIdx = gltf.nodes.length;
509
+ gltf.nodes.push({
510
+ mesh: meshIndex,
511
+ name: `feature_${fIdx}`
512
+ });
513
+
514
+ gltf.scenes[0].nodes.push(nodeIdx);
515
+ });
516
+
517
+ // Combine all buffer data
518
+ let totalSize = 0;
519
+ bufferData.forEach(buf => totalSize += buf.byteLength);
520
+
521
+ const combinedBuffer = new Uint8Array(totalSize);
522
+ let offset = 0;
523
+ bufferData.forEach(buf => {
524
+ combinedBuffer.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), offset);
525
+ offset += buf.byteLength;
526
+ });
527
+
528
+ const base64Buffer = btoa(String.fromCharCode.apply(null, combinedBuffer));
529
+ gltf.buffers = [{
530
+ byteLength: totalSize,
531
+ uri: `data:application/octet-stream;base64,${base64Buffer}`
532
+ }];
533
+
534
+ const gltfContent = JSON.stringify(gltf, null, 2);
535
+ downloadFile(gltfContent, filename, 'application/json');
536
+ }
537
+
538
+ /**
539
+ * Export features as STEP format
540
+ * Note: STEP export requires OpenCascade.js to be loaded
541
+ * @param {Array} features - Array of feature objects
542
+ * @param {Object} occt - OpenCascade.js instance (optional)
543
+ * @param {string} filename - Output filename
544
+ */
545
+ export function exportSTEP(features, occt = null, filename = 'model.step') {
546
+ if (!occt) {
547
+ console.error('STEP export requires OpenCascade.js kernel. Please load it first.');
548
+ alert('STEP export requires OpenCascade.js to be loaded.\n\nAdd this to your HTML:\n<script src="https://cdn.jsdelivr.net/npm/opencascade.js@latest/dist/opencascade.wasm.js"><\/script>');
549
+ return;
550
+ }
551
+
552
+ try {
553
+ // Create STEP writer
554
+ const step = new occt.STEPControl_Writer();
555
+
556
+ features.forEach((feature, fIdx) => {
557
+ if (!feature.mesh || !feature.mesh.geometry) return;
558
+
559
+ // This is a simplified stub - actual implementation would:
560
+ // 1. Convert Three.js mesh vertices to OCCT vertices
561
+ // 2. Build OCCT edges and wires
562
+ // 3. Create faces from wires
563
+ // 4. Combine into compound shapes
564
+ // 5. Write to STEP
565
+
566
+ // For now, just show a placeholder
567
+ console.warn(`Feature ${fIdx} conversion to OCCT shapes not yet implemented`);
568
+ });
569
+
570
+ // Placeholder: would write step.Write(filename, ...)
571
+ alert('STEP export is currently a stub.\n\nImplementation requires:\n- OpenCascade.js loaded\n- Three.js → OCCT shape conversion\n- STEP writer API calls\n\nFor now, use STL/OBJ export instead.');
572
+ } catch (error) {
573
+ console.error('STEP export error:', error);
574
+ alert(`STEP export failed: ${error.message}`);
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Export features as cycleCAD native JSON format
580
+ * This format preserves all feature parameters for re-opening and editing
581
+ * @param {Array} features - Array of feature objects with type, params, etc.
582
+ * @param {string} filename - Output filename
583
+ */
584
+ export function exportJSON(features, filename = 'model.cyclecad.json') {
585
+ const exportData = {
586
+ version: '0.1',
587
+ timestamp: new Date().toISOString(),
588
+ features: features.map(feature => ({
589
+ id: feature.id || `feature_${Math.random().toString(36).substr(2, 9)}`,
590
+ type: feature.type, // 'box', 'sphere', 'cylinder', 'sketch', etc.
591
+ name: feature.name || feature.type,
592
+ visible: feature.visible !== false,
593
+ params: feature.params || {}, // Feature-specific parameters
594
+ sketch: feature.sketch || null, // Sketch data if applicable
595
+ operations: feature.operations || [], // Applied operations (fillet, chamfer, etc.)
596
+ material: feature.material || 'default',
597
+ metadata: {
598
+ created: feature.created || new Date().toISOString(),
599
+ modified: new Date().toISOString()
600
+ }
601
+ }))
602
+ };
603
+
604
+ const jsonContent = JSON.stringify(exportData, null, 2);
605
+ downloadFile(jsonContent, filename, 'application/json');
606
+ }
607
+
608
+ /**
609
+ * Import cycleCAD native JSON format
610
+ * @param {string} jsonString - JSON string to parse
611
+ * @returns {Object} Parsed data with version and features array
612
+ */
613
+ export function importJSON(jsonString) {
614
+ try {
615
+ const data = JSON.parse(jsonString);
616
+
617
+ // Validate structure
618
+ if (!data.version || !Array.isArray(data.features)) {
619
+ throw new Error('Invalid cycleCAD JSON format. Expected { version, features }');
620
+ }
621
+
622
+ // Version compatibility check
623
+ const majorVersion = parseInt(data.version.split('.')[0]);
624
+ if (majorVersion > 0) {
625
+ console.warn(`JSON version ${data.version} may have compatibility issues`);
626
+ }
627
+
628
+ return data;
629
+ } catch (error) {
630
+ console.error('JSON import error:', error);
631
+ throw error;
632
+ }
633
+ }
634
+
635
+ /**
636
+ * Batch export all formats
637
+ * Exports model in STL, OBJ, and glTF formats simultaneously
638
+ * @param {Array} features - Array of feature objects
639
+ * @param {string} baseName - Base filename without extension
640
+ */
641
+ export function exportAllFormats(features, baseName = 'model') {
642
+ exportSTL(features, `${baseName}.stl`);
643
+ exportOBJ(features, `${baseName}.obj`);
644
+ exportGLTF(features, `${baseName}.gltf`);
645
+ exportJSON(features, `${baseName}.cyclecad.json`);
646
+ }
647
+
648
+ export default {
649
+ downloadFile,
650
+ exportSTL,
651
+ exportSTLBinary,
652
+ exportOBJ,
653
+ exportGLTF,
654
+ exportSTEP,
655
+ exportJSON,
656
+ importJSON,
657
+ exportAllFormats
658
+ };