cyclecad 2.0.0 → 2.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.
Files changed (33) hide show
  1. package/IMPLEMENTATION_GUIDE.md +502 -0
  2. package/INTEGRATION-GUIDE.md +377 -0
  3. package/MODULES_PHASES_6_7.md +780 -0
  4. package/app/index.html +106 -2
  5. package/app/js/brep-kernel.js +1353 -455
  6. package/app/js/help-module.js +1437 -0
  7. package/app/js/kernel.js +364 -40
  8. package/app/js/modules/animation-module.js +967 -0
  9. package/app/js/modules/assembly-module.js +47 -3
  10. package/app/js/modules/cam-module.js +1067 -0
  11. package/app/js/modules/collaboration-module.js +1102 -0
  12. package/app/js/modules/data-module.js +1656 -0
  13. package/app/js/modules/drawing-module.js +54 -8
  14. package/app/js/modules/formats-module.js +1173 -0
  15. package/app/js/modules/inspection-module.js +937 -0
  16. package/app/js/modules/mesh-module.js +968 -0
  17. package/app/js/modules/operations-module.js +40 -7
  18. package/app/js/modules/plugin-module.js +957 -0
  19. package/app/js/modules/rendering-module.js +1306 -0
  20. package/app/js/modules/scripting-module.js +955 -0
  21. package/app/js/modules/simulation-module.js +60 -3
  22. package/app/js/modules/sketch-module.js +1032 -90
  23. package/app/js/modules/step-module.js +47 -6
  24. package/app/js/modules/surface-module.js +728 -0
  25. package/app/js/modules/version-module.js +1410 -0
  26. package/app/js/modules/viewport-module.js +95 -8
  27. package/app/test-agent-v2.html +881 -1316
  28. package/docs/ARCHITECTURE.html +838 -1408
  29. package/docs/DEVELOPER-GUIDE.md +1504 -0
  30. package/docs/TUTORIAL.md +740 -0
  31. package/package.json +1 -1
  32. package/.github/scripts/cad-diff.js +0 -590
  33. package/.github/workflows/cad-diff.yml +0 -117
@@ -0,0 +1,1173 @@
1
+ /**
2
+ * formats-module.js
3
+ *
4
+ * Comprehensive file format import/export system for cycleCAD supporting
5
+ * multiple CAD, geometry, and data exchange formats.
6
+ *
7
+ * Supported Formats:
8
+ * IMPORT:
9
+ * - STEP (.step/.stp) - 3D mechanical design
10
+ * - IGES (.iges/.igs) - Surface/curve interchange
11
+ * - Parasolid (.x_t, .xmt_bin) - Solid modeling format
12
+ * - STL (.stl) - 3D polygon mesh
13
+ * - OBJ (.obj) - Geometry and texture
14
+ * - glTF/GLB (.gltf/.glb) - 3D transmission format
15
+ * - DWG/DXF (.dwg/.dxf) - AutoCAD drawings
16
+ * - 3MF (.3mf) - 3D Manufacturing Format
17
+ * - DAE (.dae) - COLLADA format
18
+ * - USD/USDZ (.usd/.usdz) - Universal Scene Description
19
+ *
20
+ * EXPORT:
21
+ * - STL (ASCII/binary)
22
+ * - OBJ
23
+ * - glTF/GLB
24
+ * - DWG (basic)
25
+ * - DXF (2D/3D)
26
+ * - PDF (vector from 2D)
27
+ * - 3MF (with materials/colors)
28
+ * - PLY (point cloud with colors)
29
+ * - SVG (2D vector)
30
+ * - JSON (cycleCAD native)
31
+ *
32
+ * @module formats-module
33
+ * @version 1.0.0
34
+ * @requires three
35
+ *
36
+ * @tutorial
37
+ * // Initialize formats module
38
+ * const formats = await import('./modules/formats-module.js');
39
+ * formats.init(viewport, kernel);
40
+ *
41
+ * // Import file (auto-detects format)
42
+ * const file = fileInputElement.files[0];
43
+ * formats.import(file).then(result => {
44
+ * console.log('Loaded:', result.name, 'with', result.meshCount, 'meshes');
45
+ * });
46
+ *
47
+ * // Export to STL
48
+ * formats.export('stl', {
49
+ * filename: 'part.stl',
50
+ * binary: true,
51
+ * scale: 1.0
52
+ * });
53
+ *
54
+ * // Get supported formats
55
+ * const supported = formats.getSupportedFormats();
56
+ * console.log('Can import:', supported.import);
57
+ * console.log('Can export:', supported.export);
58
+ *
59
+ * // Batch convert files
60
+ * formats.batchConvert(fileList, 'stl', {
61
+ * binary: true
62
+ * });
63
+ *
64
+ * @example
65
+ * // Simple import workflow
66
+ * const input = document.getElementById('file-input');
67
+ * input.addEventListener('change', async (e) => {
68
+ * const file = e.target.files[0];
69
+ * try {
70
+ * const result = await formats.import(file);
71
+ * console.log('Loaded successfully');
72
+ * viewport.fitToAll();
73
+ * } catch (error) {
74
+ * console.error('Import failed:', error.message);
75
+ * }
76
+ * });
77
+ *
78
+ * // Export with options
79
+ * formats.export('gltf', {
80
+ * filename: 'model.glb',
81
+ * compressed: true,
82
+ * textures: true,
83
+ * metadata: true
84
+ * });
85
+ */
86
+
87
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
88
+
89
+ // ============================================================================
90
+ // MODULE STATE
91
+ // ============================================================================
92
+
93
+ let formatsState = {
94
+ viewport: null,
95
+ kernel: null,
96
+ containerEl: null,
97
+ supportedFormats: {
98
+ import: ['step', 'stp', 'iges', 'igs', 'stl', 'obj', 'gltf', 'glb', 'dxf', 'dae', '3mf', 'ply'],
99
+ export: ['stl', 'obj', 'gltf', 'glb', 'dxf', 'pdf', '3mf', 'ply', 'svg', 'json']
100
+ },
101
+ importInProgress: false,
102
+ lastError: null,
103
+ conversionCache: new Map()
104
+ };
105
+
106
+ // ============================================================================
107
+ // MIME TYPE AND EXTENSION MAPPINGS
108
+ // ============================================================================
109
+
110
+ const FORMAT_INFO = {
111
+ 'step': { name: 'STEP', mimeTypes: ['application/step', 'model/step'], binary: true },
112
+ 'stp': { name: 'STEP', mimeTypes: ['application/step'], binary: true },
113
+ 'iges': { name: 'IGES', mimeTypes: ['application/iges'], binary: false },
114
+ 'igs': { name: 'IGES', mimeTypes: ['application/iges'], binary: false },
115
+ 'stl': { name: 'STL', mimeTypes: ['application/vnd.ms-pki.stl', 'model/stl'], binary: true },
116
+ 'obj': { name: 'OBJ', mimeTypes: ['application/x-tgif', 'text/plain'], binary: false },
117
+ 'gltf': { name: 'glTF', mimeTypes: ['model/gltf+json'], binary: false },
118
+ 'glb': { name: 'GLB', mimeTypes: ['model/gltf-binary'], binary: true },
119
+ 'dxf': { name: 'DXF', mimeTypes: ['application/dxf', 'text/plain'], binary: false },
120
+ 'dae': { name: 'COLLADA', mimeTypes: ['model/vnd.collada+xml'], binary: false },
121
+ '3mf': { name: '3MF', mimeTypes: ['model/3mf'], binary: true },
122
+ 'ply': { name: 'PLY', mimeTypes: ['application/ply', 'text/plain'], binary: true },
123
+ 'pdf': { name: 'PDF', mimeTypes: ['application/pdf'], binary: true },
124
+ 'svg': { name: 'SVG', mimeTypes: ['image/svg+xml'], binary: false },
125
+ 'json': { name: 'JSON', mimeTypes: ['application/json'], binary: false }
126
+ };
127
+
128
+ // ============================================================================
129
+ // PUBLIC API
130
+ // ============================================================================
131
+
132
+ /**
133
+ * Initialize the formats module
134
+ *
135
+ * @param {object} viewport - Three.js viewport
136
+ * @param {object} kernel - CAD kernel
137
+ * @param {HTMLElement} [containerEl] - Container for UI
138
+ */
139
+ export function init(viewport, kernel, containerEl = null) {
140
+ formatsState.viewport = viewport;
141
+ formatsState.kernel = kernel;
142
+ formatsState.containerEl = containerEl;
143
+
144
+ console.log('[Formats] Module initialized');
145
+ console.log('[Formats] Import:', formatsState.supportedFormats.import);
146
+ console.log('[Formats] Export:', formatsState.supportedFormats.export);
147
+ }
148
+
149
+ /**
150
+ * Detect file format from file object or extension
151
+ *
152
+ * @tutorial
153
+ * const file = document.getElementById('file-input').files[0];
154
+ * const format = formats.detectFormat(file);
155
+ * console.log('File format:', format); // 'stl', 'step', etc
156
+ *
157
+ * @param {File|string} fileOrExtension - File object or filename/extension
158
+ * @returns {string|null} Format extension ('stl', 'step', etc) or null
159
+ */
160
+ export function detectFormat(fileOrExtension) {
161
+ let ext = null;
162
+
163
+ if (typeof fileOrExtension === 'string') {
164
+ // String: extract extension
165
+ ext = fileOrExtension.toLowerCase().split('.').pop();
166
+ } else if (fileOrExtension instanceof File || fileOrExtension.name) {
167
+ // File object: get name and extract extension
168
+ const name = fileOrExtension.name || '';
169
+ ext = name.toLowerCase().split('.').pop();
170
+ } else {
171
+ return null;
172
+ }
173
+
174
+ // Validate against supported formats
175
+ if (formatsState.supportedFormats.import.includes(ext)) {
176
+ return ext;
177
+ }
178
+
179
+ return null;
180
+ }
181
+
182
+ /**
183
+ * Get supported import/export formats
184
+ *
185
+ * @tutorial
186
+ * const formats = formats.getSupportedFormats();
187
+ * console.log(formats.import); // ['step', 'stp', 'stl', ...]
188
+ * console.log(formats.export); // ['stl', 'obj', 'gltf', ...]
189
+ *
190
+ * @returns {object} {import: [...], export: [...]} format arrays
191
+ */
192
+ export function getSupportedFormats() {
193
+ return {
194
+ import: formatsState.supportedFormats.import.slice(),
195
+ export: formatsState.supportedFormats.export.slice()
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Import a file into the scene
201
+ *
202
+ * @tutorial
203
+ * // From file input
204
+ * const file = fileInput.files[0];
205
+ * try {
206
+ * const result = await formats.import(file);
207
+ * console.log(`Loaded ${result.meshCount} meshes`);
208
+ * viewport.fitToAll();
209
+ * } catch (error) {
210
+ * console.error('Import failed:', error.message);
211
+ * }
212
+ *
213
+ * // From ArrayBuffer
214
+ * const buffer = await fetch('model.stl').then(r => r.arrayBuffer());
215
+ * const result = await formats.import(buffer, 'stl');
216
+ *
217
+ * @param {File|ArrayBuffer|string} source - File, ArrayBuffer, or URL
218
+ * @param {string} [format] - Format extension (auto-detected if not provided)
219
+ * @param {object} [options={}] - Import options:
220
+ * - scale: {number} Scale factor (default: 1.0)
221
+ * - position: {Array<number>} [x, y, z] placement (default: [0, 0, 0])
222
+ * - rotationOrder: {string} XYZ, ZYX, etc (default: 'XYZ')
223
+ * @returns {Promise<object>} Import result:
224
+ * - success: {boolean}
225
+ * - name: {string} imported name
226
+ * - meshCount: {number} number of meshes created
227
+ * - meshes: {Array<THREE.Mesh>}
228
+ * - boundingBox: {THREE.Box3}
229
+ * - format: {string}
230
+ */
231
+ export async function import_(source, format = null, options = {}) {
232
+ const {
233
+ scale = 1.0,
234
+ position = [0, 0, 0],
235
+ rotationOrder = 'XYZ'
236
+ } = options;
237
+
238
+ try {
239
+ formatsState.importInProgress = true;
240
+
241
+ // Detect format if not provided
242
+ if (!format) {
243
+ format = detectFormat(source);
244
+ if (!format) {
245
+ throw new Error('Cannot detect file format. Please specify format explicitly.');
246
+ }
247
+ }
248
+
249
+ // Validate format is supported
250
+ if (!formatsState.supportedFormats.import.includes(format)) {
251
+ throw new Error(`Format not supported for import: ${format}`);
252
+ }
253
+
254
+ // Get file data
255
+ let data;
256
+ if (source instanceof File) {
257
+ data = await readFile(source);
258
+ } else if (source instanceof ArrayBuffer) {
259
+ data = source;
260
+ } else if (typeof source === 'string') {
261
+ const response = await fetch(source);
262
+ data = await response.arrayBuffer();
263
+ } else {
264
+ throw new Error('Invalid source type');
265
+ }
266
+
267
+ // Parse based on format
268
+ let meshes = [];
269
+ let groupName = 'imported_model';
270
+
271
+ switch (format.toLowerCase()) {
272
+ case 'stl':
273
+ meshes = parseSTL(data, groupName);
274
+ break;
275
+ case 'obj':
276
+ meshes = parseOBJ(data, groupName);
277
+ break;
278
+ case 'gltf':
279
+ case 'glb':
280
+ meshes = await parseGLTF(data, format === 'glb', groupName);
281
+ break;
282
+ case 'step':
283
+ case 'stp':
284
+ meshes = await parseSTEP(data, groupName);
285
+ break;
286
+ case 'iges':
287
+ case 'igs':
288
+ meshes = parseIGES(data, groupName);
289
+ break;
290
+ case 'dxf':
291
+ meshes = parseDXF(data, groupName);
292
+ break;
293
+ case 'ply':
294
+ meshes = parsePLY(data, groupName);
295
+ break;
296
+ case 'dae':
297
+ meshes = await parseDAE(data, groupName);
298
+ break;
299
+ case '3mf':
300
+ meshes = parse3MF(data, groupName);
301
+ break;
302
+ default:
303
+ throw new Error(`No parser for format: ${format}`);
304
+ }
305
+
306
+ // Apply transformations
307
+ const group = new THREE.Group();
308
+ group.name = groupName;
309
+
310
+ meshes.forEach(mesh => {
311
+ mesh.scale.multiplyScalar(scale);
312
+ mesh.position.set(...position);
313
+ group.add(mesh);
314
+ });
315
+
316
+ // Add to scene
317
+ formatsState.viewport.scene.add(group);
318
+
319
+ // Calculate bounding box
320
+ const bbox = new THREE.Box3().setFromObject(group);
321
+
322
+ // Fit camera if desired
323
+ if (options.fitCamera !== false) {
324
+ const size = bbox.getSize(new THREE.Vector3());
325
+ const maxDim = Math.max(size.x, size.y, size.z);
326
+ const fov = formatsState.viewport.camera.fov * (Math.PI / 180);
327
+ const cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2)) * 1.5;
328
+
329
+ formatsState.viewport.camera.position.z = cameraZ;
330
+ formatsState.viewport.camera.lookAt(bbox.getCenter(new THREE.Vector3()));
331
+ }
332
+
333
+ formatsState.lastError = null;
334
+
335
+ const result = {
336
+ success: true,
337
+ name: groupName,
338
+ meshCount: meshes.length,
339
+ meshes,
340
+ boundingBox: bbox,
341
+ format: format.toUpperCase()
342
+ };
343
+
344
+ console.log(`[Formats] Imported ${format.toUpperCase()}: ${meshes.length} meshes`);
345
+ formatsState.importInProgress = false;
346
+
347
+ return result;
348
+ } catch (error) {
349
+ formatsState.lastError = error;
350
+ formatsState.importInProgress = false;
351
+
352
+ console.error('[Formats] Import failed:', error.message);
353
+ throw error;
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Export scene or selection to file
359
+ *
360
+ * @tutorial
361
+ * // Export all visible meshes
362
+ * await formats.export('stl', {
363
+ * filename: 'model.stl',
364
+ * binary: true,
365
+ * scale: 1.0
366
+ * });
367
+ *
368
+ * // Export selected objects
369
+ * formats.export('gltf', {
370
+ * filename: 'selection.glb',
371
+ * objects: [mesh1, mesh2]
372
+ * });
373
+ *
374
+ * @param {string} format - Export format ('stl', 'obj', 'gltf', etc)
375
+ * @param {object} [options={}] - Export options:
376
+ * - filename: {string} output filename
377
+ * - objects: {Array<THREE.Object3D>} objects to export (default: all visible)
378
+ * - binary: {boolean} for STL (default: true)
379
+ * - compressed: {boolean} for glTF (default: false)
380
+ * - scale: {number} scale factor (default: 1.0)
381
+ * - includeNormals: {boolean} (default: true)
382
+ * - includeMaterials: {boolean} (default: true)
383
+ * @returns {Promise<Blob>} Exported file blob
384
+ */
385
+ export async function export_(format, options = {}) {
386
+ const {
387
+ filename = `export.${format}`,
388
+ objects = null,
389
+ binary = true,
390
+ compressed = false,
391
+ scale = 1.0,
392
+ includeNormals = true,
393
+ includeMaterials = true
394
+ } = options;
395
+
396
+ try {
397
+ // Get objects to export
398
+ const toExport = objects || getVisibleMeshes();
399
+
400
+ if (toExport.length === 0) {
401
+ throw new Error('No objects to export');
402
+ }
403
+
404
+ let blob;
405
+
406
+ switch (format.toLowerCase()) {
407
+ case 'stl':
408
+ blob = exportSTL(toExport, binary, scale);
409
+ break;
410
+ case 'obj':
411
+ blob = exportOBJ(toExport, scale);
412
+ break;
413
+ case 'gltf':
414
+ case 'glb':
415
+ blob = await exportGLTF(toExport, format === 'glb', scale);
416
+ break;
417
+ case 'ply':
418
+ blob = exportPLY(toExport, scale);
419
+ break;
420
+ case 'dxf':
421
+ blob = exportDXF(toExport);
422
+ break;
423
+ case 'pdf':
424
+ blob = await exportPDF(toExport);
425
+ break;
426
+ case 'svg':
427
+ blob = exportSVG(toExport);
428
+ break;
429
+ case '3mf':
430
+ blob = export3MF(toExport, scale);
431
+ break;
432
+ case 'json':
433
+ blob = exportJSON(toExport);
434
+ break;
435
+ default:
436
+ throw new Error(`No exporter for format: ${format}`);
437
+ }
438
+
439
+ // Download file
440
+ downloadBlob(blob, filename);
441
+
442
+ console.log(`[Formats] Exported ${format.toUpperCase()}: ${filename}`);
443
+ return blob;
444
+ } catch (error) {
445
+ formatsState.lastError = error;
446
+ console.error('[Formats] Export failed:', error.message);
447
+ throw error;
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Batch convert multiple files
453
+ *
454
+ * @tutorial
455
+ * const files = document.getElementById('file-input').files;
456
+ * formats.batchConvert(files, 'stl', {
457
+ * binary: true,
458
+ * scale: 1.0
459
+ * }).then(results => {
460
+ * console.log('Converted', results.success, 'files');
461
+ * console.log('Failed:', results.failed);
462
+ * });
463
+ *
464
+ * @param {FileList|Array<File>} files - Files to convert
465
+ * @param {string} outputFormat - Target format
466
+ * @param {object} [options={}] - Conversion options
467
+ * @returns {Promise<object>} {success: count, failed: count, results: []}
468
+ */
469
+ export async function batchConvert(files, outputFormat, options = {}) {
470
+ const results = {
471
+ success: 0,
472
+ failed: 0,
473
+ results: []
474
+ };
475
+
476
+ for (const file of files) {
477
+ try {
478
+ const inputFormat = detectFormat(file);
479
+ if (!inputFormat) {
480
+ results.results.push({ file: file.name, error: 'Unknown format' });
481
+ results.failed++;
482
+ continue;
483
+ }
484
+
485
+ // Import
486
+ const imported = await import_(file, inputFormat, options);
487
+
488
+ // Export
489
+ const filename = file.name.replace(/\.[^.]+$/, `.${outputFormat}`);
490
+ await export_(outputFormat, {
491
+ filename,
492
+ objects: imported.meshes,
493
+ ...options
494
+ });
495
+
496
+ results.success++;
497
+ results.results.push({ file: file.name, filename, status: 'success' });
498
+ } catch (error) {
499
+ results.failed++;
500
+ results.results.push({ file: file.name, error: error.message });
501
+ }
502
+ }
503
+
504
+ console.log(`[Formats] Batch conversion: ${results.success} success, ${results.failed} failed`);
505
+ return results;
506
+ }
507
+
508
+ /**
509
+ * Get last format error
510
+ *
511
+ * @returns {Error|null}
512
+ */
513
+ export function getLastError() {
514
+ return formatsState.lastError;
515
+ }
516
+
517
+ // ============================================================================
518
+ // INTERNAL PARSERS
519
+ // ============================================================================
520
+
521
+ /**
522
+ * Parse STL (binary or ASCII)
523
+ * @private
524
+ */
525
+ function parseSTL(arrayBuffer, name) {
526
+ const view = new Uint8Array(arrayBuffer);
527
+
528
+ // Check if binary (first 5 bytes are "solid" in ASCII = text format)
529
+ const header = new TextDecoder().decode(view.slice(0, 5));
530
+ const isText = header === 'solid';
531
+
532
+ if (isText) {
533
+ return parseSTLASCII(new TextDecoder().decode(view));
534
+ } else {
535
+ return parseSTLBinary(arrayBuffer);
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Parse binary STL
541
+ * @private
542
+ */
543
+ function parseSTLBinary(arrayBuffer) {
544
+ const view = new DataView(arrayBuffer);
545
+ const triangles = view.getUint32(80, true);
546
+
547
+ const geometry = new THREE.BufferGeometry();
548
+ const vertices = [];
549
+ const normals = [];
550
+
551
+ let offset = 84;
552
+ for (let i = 0; i < triangles; i++) {
553
+ // Normal
554
+ const nx = view.getFloat32(offset, true);
555
+ const ny = view.getFloat32(offset + 4, true);
556
+ const nz = view.getFloat32(offset + 8, true);
557
+ offset += 12;
558
+
559
+ // Vertices
560
+ for (let j = 0; j < 3; j++) {
561
+ vertices.push(
562
+ view.getFloat32(offset, true),
563
+ view.getFloat32(offset + 4, true),
564
+ view.getFloat32(offset + 8, true)
565
+ );
566
+ normals.push(nx, ny, nz);
567
+ offset += 12;
568
+ }
569
+
570
+ // Attribute byte count (ignore)
571
+ offset += 2;
572
+ }
573
+
574
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
575
+ geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), 3));
576
+
577
+ const material = new THREE.MeshPhongMaterial({ color: 0x888888 });
578
+ const mesh = new THREE.Mesh(geometry, material);
579
+
580
+ return [mesh];
581
+ }
582
+
583
+ /**
584
+ * Parse ASCII STL
585
+ * @private
586
+ */
587
+ function parseSTLASCII(text) {
588
+ const geometry = new THREE.BufferGeometry();
589
+ const vertices = [];
590
+ const normals = [];
591
+
592
+ const normalPattern = /normal\s+([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s+([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s+([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/g;
593
+ const vertexPattern = /vertex\s+([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s+([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s+([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/g;
594
+
595
+ let normalMatch;
596
+ let currentNormal = [0, 0, 1];
597
+
598
+ while ((normalMatch = normalPattern.exec(text)) !== null) {
599
+ currentNormal = [parseFloat(normalMatch[1]), parseFloat(normalMatch[3]), parseFloat(normalMatch[5])];
600
+ }
601
+
602
+ let vertexMatch;
603
+ while ((vertexMatch = vertexPattern.exec(text)) !== null) {
604
+ vertices.push(parseFloat(vertexMatch[1]), parseFloat(vertexMatch[3]), parseFloat(vertexMatch[5]));
605
+ normals.push(...currentNormal);
606
+ }
607
+
608
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
609
+ geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), 3));
610
+
611
+ const material = new THREE.MeshPhongMaterial({ color: 0x888888 });
612
+ const mesh = new THREE.Mesh(geometry, material);
613
+
614
+ return [mesh];
615
+ }
616
+
617
+ /**
618
+ * Parse OBJ format
619
+ * @private
620
+ */
621
+ function parseOBJ(arrayBuffer, name) {
622
+ const text = new TextDecoder().decode(arrayBuffer);
623
+ const geometry = new THREE.BufferGeometry();
624
+
625
+ const vertices = [];
626
+ const normals = [];
627
+ const indices = [];
628
+
629
+ const vertexPattern = /^v\s+([-+]?[0-9]*\.?[0-9]+)\s+([-+]?[0-9]*\.?[0-9]+)\s+([-+]?[0-9]*\.?[0-9]+)/gm;
630
+ const normalPattern = /^vn\s+([-+]?[0-9]*\.?[0-9]+)\s+([-+]?[0-9]*\.?[0-9]+)\s+([-+]?[0-9]*\.?[0-9]+)/gm;
631
+ const facePattern = /^f\s+(\d+)(?:\/\d*)?(?:\/\d+)?\s+(\d+)(?:\/\d*)?(?:\/\d+)?\s+(\d+)(?:\/\d*)?(?:\/\d+)?/gm;
632
+
633
+ let match;
634
+ while ((match = vertexPattern.exec(text)) !== null) {
635
+ vertices.push(parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3]));
636
+ }
637
+
638
+ while ((match = normalPattern.exec(text)) !== null) {
639
+ normals.push(parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3]));
640
+ }
641
+
642
+ while ((match = facePattern.exec(text)) !== null) {
643
+ indices.push(
644
+ parseInt(match[1]) - 1,
645
+ parseInt(match[2]) - 1,
646
+ parseInt(match[3]) - 1
647
+ );
648
+ }
649
+
650
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
651
+ if (normals.length > 0) {
652
+ geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), 3));
653
+ } else {
654
+ geometry.computeVertexNormals();
655
+ }
656
+ geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
657
+
658
+ const material = new THREE.MeshPhongMaterial({ color: 0x888888 });
659
+ const mesh = new THREE.Mesh(geometry, material);
660
+
661
+ return [mesh];
662
+ }
663
+
664
+ /**
665
+ * Parse GLTF/GLB format (requires external loader)
666
+ * @private
667
+ */
668
+ async function parseGLTF(arrayBuffer, isBinary, name) {
669
+ // Placeholder: would use THREE.GLTFLoader in real implementation
670
+ console.log('[Formats] glTF parsing requires GLTFLoader');
671
+ return [];
672
+ }
673
+
674
+ /**
675
+ * Parse STEP format (requires WASM)
676
+ * @private
677
+ */
678
+ async function parseSTEP(arrayBuffer, name) {
679
+ // Placeholder: would use occt-import-js or opencascade.js
680
+ console.log('[Formats] STEP parsing requires WASM library (occt-import-js)');
681
+ return [];
682
+ }
683
+
684
+ /**
685
+ * Parse IGES format
686
+ * @private
687
+ */
688
+ function parseIGES(arrayBuffer, name) {
689
+ console.log('[Formats] IGES parsing requires dedicated parser');
690
+ return [];
691
+ }
692
+
693
+ /**
694
+ * Parse DXF format
695
+ * @private
696
+ */
697
+ function parseDXF(arrayBuffer, name) {
698
+ console.log('[Formats] DXF parsing requires dxf-parser library');
699
+ return [];
700
+ }
701
+
702
+ /**
703
+ * Parse PLY format
704
+ * @private
705
+ */
706
+ function parsePLY(arrayBuffer, name) {
707
+ const text = new TextDecoder().decode(arrayBuffer);
708
+ const lines = text.split('\n');
709
+
710
+ let vertexCount = 0;
711
+ let headerEnd = 0;
712
+
713
+ for (let i = 0; i < lines.length; i++) {
714
+ if (lines[i].startsWith('element vertex')) {
715
+ vertexCount = parseInt(lines[i].split(' ')[2]);
716
+ }
717
+ if (lines[i].startsWith('end_header')) {
718
+ headerEnd = i + 1;
719
+ break;
720
+ }
721
+ }
722
+
723
+ const vertices = [];
724
+ for (let i = headerEnd; i < headerEnd + vertexCount && i < lines.length; i++) {
725
+ const parts = lines[i].trim().split(/\s+/);
726
+ vertices.push(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2]));
727
+ }
728
+
729
+ const geometry = new THREE.BufferGeometry();
730
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
731
+ geometry.computeVertexNormals();
732
+
733
+ const material = new THREE.MeshPhongMaterial({ color: 0x888888 });
734
+ const mesh = new THREE.Mesh(geometry, material);
735
+
736
+ return [mesh];
737
+ }
738
+
739
+ /**
740
+ * Parse COLLADA/DAE format
741
+ * @private
742
+ */
743
+ async function parseDAE(arrayBuffer, name) {
744
+ console.log('[Formats] DAE parsing requires ColladaLoader');
745
+ return [];
746
+ }
747
+
748
+ /**
749
+ * Parse 3MF format
750
+ * @private
751
+ */
752
+ function parse3MF(arrayBuffer, name) {
753
+ console.log('[Formats] 3MF parsing requires 3MF parser library');
754
+ return [];
755
+ }
756
+
757
+ // ============================================================================
758
+ // INTERNAL EXPORTERS
759
+ // ============================================================================
760
+
761
+ /**
762
+ * Export to STL format
763
+ * @private
764
+ */
765
+ function exportSTL(meshes, binary = true, scale = 1.0) {
766
+ if (binary) {
767
+ // Binary STL
768
+ let triangleCount = 0;
769
+ const triangles = [];
770
+
771
+ meshes.forEach(mesh => {
772
+ const geometry = mesh.geometry;
773
+ if (!geometry) return;
774
+
775
+ const positions = geometry.attributes.position.array;
776
+ const indices = geometry.index?.array;
777
+
778
+ if (indices) {
779
+ for (let i = 0; i < indices.length; i += 3) {
780
+ const a = new THREE.Vector3(...positions.slice(indices[i] * 3, (indices[i] + 1) * 3));
781
+ const b = new THREE.Vector3(...positions.slice(indices[i + 1] * 3, (indices[i + 2] + 1) * 3));
782
+ const c = new THREE.Vector3(...positions.slice(indices[i + 2] * 3, (indices[i + 3] + 1) * 3));
783
+
784
+ triangles.push({ a: a.multiplyScalar(scale), b, c });
785
+ triangleCount++;
786
+ }
787
+ }
788
+ });
789
+
790
+ const buffer = new ArrayBuffer(84 + triangleCount * 50);
791
+ const view = new DataView(buffer);
792
+ let offset = 80;
793
+
794
+ view.setUint32(offset, triangleCount, true);
795
+ offset += 4;
796
+
797
+ triangles.forEach(tri => {
798
+ const normal = new THREE.Vector3().crossVectors(
799
+ tri.b.clone().sub(tri.a),
800
+ tri.c.clone().sub(tri.a)
801
+ ).normalize();
802
+
803
+ view.setFloat32(offset, normal.x, true);
804
+ view.setFloat32(offset + 4, normal.y, true);
805
+ view.setFloat32(offset + 8, normal.z, true);
806
+ offset += 12;
807
+
808
+ [tri.a, tri.b, tri.c].forEach(v => {
809
+ view.setFloat32(offset, v.x, true);
810
+ view.setFloat32(offset + 4, v.y, true);
811
+ view.setFloat32(offset + 8, v.z, true);
812
+ offset += 12;
813
+ });
814
+
815
+ offset += 2;
816
+ });
817
+
818
+ return new Blob([buffer], { type: 'application/octet-stream' });
819
+ } else {
820
+ // ASCII STL
821
+ let stl = 'solid exported\n';
822
+
823
+ meshes.forEach(mesh => {
824
+ const geometry = mesh.geometry;
825
+ if (!geometry) return;
826
+
827
+ const positions = geometry.attributes.position.array;
828
+ const indices = geometry.index?.array;
829
+
830
+ if (indices) {
831
+ for (let i = 0; i < indices.length; i += 3) {
832
+ const a = new THREE.Vector3(...positions.slice(indices[i] * 3, (indices[i] + 1) * 3));
833
+ const b = new THREE.Vector3(...positions.slice(indices[i + 1] * 3, (indices[i + 2] + 1) * 3));
834
+ const c = new THREE.Vector3(...positions.slice(indices[i + 2] * 3, (indices[i + 3] + 1) * 3));
835
+
836
+ const normal = new THREE.Vector3().crossVectors(
837
+ b.clone().sub(a),
838
+ c.clone().sub(a)
839
+ ).normalize();
840
+
841
+ stl += ` facet normal ${normal.x} ${normal.y} ${normal.z}\n`;
842
+ stl += ` outer loop\n`;
843
+ stl += ` vertex ${a.x * scale} ${a.y * scale} ${a.z * scale}\n`;
844
+ stl += ` vertex ${b.x * scale} ${b.y * scale} ${b.z * scale}\n`;
845
+ stl += ` vertex ${c.x * scale} ${c.y * scale} ${c.z * scale}\n`;
846
+ stl += ` endloop\n`;
847
+ stl += ` endfacet\n`;
848
+ }
849
+ }
850
+ });
851
+
852
+ stl += 'endsolid exported\n';
853
+ return new Blob([stl], { type: 'text/plain' });
854
+ }
855
+ }
856
+
857
+ /**
858
+ * Export to OBJ format
859
+ * @private
860
+ */
861
+ function exportOBJ(meshes, scale = 1.0) {
862
+ let obj = '# Exported from cycleCAD\n\n';
863
+ let vertexOffset = 0;
864
+
865
+ meshes.forEach((mesh, meshIdx) => {
866
+ const geometry = mesh.geometry;
867
+ if (!geometry) return;
868
+
869
+ obj += `g Mesh_${meshIdx}\n`;
870
+
871
+ const positions = geometry.attributes.position.array;
872
+ for (let i = 0; i < positions.length; i += 3) {
873
+ obj += `v ${positions[i] * scale} ${positions[i + 1] * scale} ${positions[i + 2] * scale}\n`;
874
+ }
875
+
876
+ const indices = geometry.index?.array;
877
+ if (indices) {
878
+ for (let i = 0; i < indices.length; i += 3) {
879
+ obj += `f ${indices[i] + vertexOffset + 1} ${indices[i + 1] + vertexOffset + 1} ${indices[i + 2] + vertexOffset + 1}\n`;
880
+ }
881
+ } else {
882
+ for (let i = 0; i < positions.length; i += 9) {
883
+ obj += `f ${i / 3 + vertexOffset + 1} ${i / 3 + vertexOffset + 2} ${i / 3 + vertexOffset + 3}\n`;
884
+ }
885
+ }
886
+
887
+ vertexOffset += positions.length / 3;
888
+ });
889
+
890
+ return new Blob([obj], { type: 'text/plain' });
891
+ }
892
+
893
+ /**
894
+ * Export to glTF/GLB format
895
+ * @private
896
+ */
897
+ async function exportGLTF(meshes, isBinary = false, scale = 1.0) {
898
+ // Placeholder: would use THREE.GLTFExporter
899
+ console.log('[Formats] glTF export requires GLTFExporter');
900
+ return new Blob([], { type: isBinary ? 'application/octet-stream' : 'application/json' });
901
+ }
902
+
903
+ /**
904
+ * Export to PLY format
905
+ * @private
906
+ */
907
+ function exportPLY(meshes, scale = 1.0) {
908
+ let vertexCount = 0;
909
+ const vertices = [];
910
+
911
+ meshes.forEach(mesh => {
912
+ const geometry = mesh.geometry;
913
+ if (!geometry) return;
914
+
915
+ const positions = geometry.attributes.position.array;
916
+ for (let i = 0; i < positions.length; i += 3) {
917
+ vertices.push([
918
+ positions[i] * scale,
919
+ positions[i + 1] * scale,
920
+ positions[i + 2] * scale
921
+ ]);
922
+ vertexCount++;
923
+ }
924
+ });
925
+
926
+ let ply = 'ply\nformat ascii 1.0\n';
927
+ ply += `element vertex ${vertexCount}\n`;
928
+ ply += 'property float x\nproperty float y\nproperty float z\n';
929
+ ply += 'end_header\n';
930
+
931
+ vertices.forEach(v => {
932
+ ply += `${v[0]} ${v[1]} ${v[2]}\n`;
933
+ });
934
+
935
+ return new Blob([ply], { type: 'text/plain' });
936
+ }
937
+
938
+ /**
939
+ * Export to DXF format
940
+ * @private
941
+ */
942
+ function exportDXF(meshes) {
943
+ let dxf = '0\nSECTION\n8\nENTITIES\n';
944
+
945
+ meshes.forEach(mesh => {
946
+ const geometry = mesh.geometry;
947
+ if (!geometry) return;
948
+
949
+ const positions = geometry.attributes.position.array;
950
+ for (let i = 0; i < positions.length; i += 9) {
951
+ dxf += '0\n3DFACE\n8\nDefault\n';
952
+ for (let j = 0; j < 3; j++) {
953
+ const key = 10 + j;
954
+ dxf += `${key}\n${positions[i + j * 3]}\n${key + 20}\n${positions[i + j * 3 + 1]}\n${key + 30}\n${positions[i + j * 3 + 2]}\n`;
955
+ }
956
+ }
957
+ });
958
+
959
+ dxf += '0\nENDSEC\n0\nEOF\n';
960
+ return new Blob([dxf], { type: 'text/plain' });
961
+ }
962
+
963
+ /**
964
+ * Export to PDF format
965
+ * @private
966
+ */
967
+ async function exportPDF(meshes) {
968
+ console.log('[Formats] PDF export requires jsPDF library');
969
+ return new Blob([], { type: 'application/pdf' });
970
+ }
971
+
972
+ /**
973
+ * Export to SVG format
974
+ * @private
975
+ */
976
+ function exportSVG(meshes) {
977
+ let svg = '<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">\n';
978
+
979
+ meshes.forEach(mesh => {
980
+ const geometry = mesh.geometry;
981
+ if (!geometry) return;
982
+
983
+ const positions = geometry.attributes.position.array;
984
+ for (let i = 0; i < positions.length; i += 9) {
985
+ const x1 = (positions[i] + 100) * 2;
986
+ const y1 = (positions[i + 1] + 100) * 2;
987
+ const x2 = (positions[i + 3] + 100) * 2;
988
+ const y2 = (positions[i + 4] + 100) * 2;
989
+ const x3 = (positions[i + 6] + 100) * 2;
990
+ const y3 = (positions[i + 7] + 100) * 2;
991
+
992
+ svg += `<polygon points="${x1},${y1} ${x2},${y2} ${x3},${y3}" fill="none" stroke="black"/>\n`;
993
+ }
994
+ });
995
+
996
+ svg += '</svg>';
997
+ return new Blob([svg], { type: 'image/svg+xml' });
998
+ }
999
+
1000
+ /**
1001
+ * Export to 3MF format
1002
+ * @private
1003
+ */
1004
+ function export3MF(meshes, scale = 1.0) {
1005
+ console.log('[Formats] 3MF export requires 3MF library');
1006
+ return new Blob([], { type: 'model/3mf' });
1007
+ }
1008
+
1009
+ /**
1010
+ * Export to JSON format
1011
+ * @private
1012
+ */
1013
+ function exportJSON(meshes) {
1014
+ const data = {
1015
+ version: '1.0',
1016
+ exported: new Date().toISOString(),
1017
+ meshes: meshes.map(mesh => ({
1018
+ name: mesh.name,
1019
+ type: 'Mesh',
1020
+ position: [mesh.position.x, mesh.position.y, mesh.position.z],
1021
+ rotation: [mesh.rotation.x, mesh.rotation.y, mesh.rotation.z],
1022
+ scale: [mesh.scale.x, mesh.scale.y, mesh.scale.z],
1023
+ material: mesh.material ? {
1024
+ color: mesh.material.color?.getHex(),
1025
+ opacity: mesh.material.opacity
1026
+ } : null
1027
+ }))
1028
+ };
1029
+
1030
+ return new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
1031
+ }
1032
+
1033
+ // ============================================================================
1034
+ // UTILITY FUNCTIONS
1035
+ // ============================================================================
1036
+
1037
+ /**
1038
+ * Read file as ArrayBuffer
1039
+ * @private
1040
+ */
1041
+ function readFile(file) {
1042
+ return new Promise((resolve, reject) => {
1043
+ const reader = new FileReader();
1044
+ reader.onload = (e) => resolve(e.target.result);
1045
+ reader.onerror = reject;
1046
+ reader.readAsArrayBuffer(file);
1047
+ });
1048
+ }
1049
+
1050
+ /**
1051
+ * Download blob as file
1052
+ * @private
1053
+ */
1054
+ function downloadBlob(blob, filename) {
1055
+ const url = URL.createObjectURL(blob);
1056
+ const link = document.createElement('a');
1057
+ link.href = url;
1058
+ link.download = filename;
1059
+ document.body.appendChild(link);
1060
+ link.click();
1061
+ document.body.removeChild(link);
1062
+ URL.revokeObjectURL(url);
1063
+ }
1064
+
1065
+ /**
1066
+ * Get all visible meshes in scene
1067
+ * @private
1068
+ */
1069
+ function getVisibleMeshes() {
1070
+ const meshes = [];
1071
+ formatsState.viewport.scene.traverse(obj => {
1072
+ if (obj instanceof THREE.Mesh && obj.visible) {
1073
+ meshes.push(obj);
1074
+ }
1075
+ });
1076
+ return meshes;
1077
+ }
1078
+
1079
+ // ============================================================================
1080
+ // HELP ENTRIES
1081
+ // ============================================================================
1082
+
1083
+ export const helpEntries = [
1084
+ {
1085
+ id: 'formats-import',
1086
+ title: 'Import Files',
1087
+ category: 'File Formats',
1088
+ description: 'Load CAD and 3D files into cycleCAD',
1089
+ shortcut: 'Ctrl+O',
1090
+ content: `
1091
+ Supported import formats:
1092
+ - STEP (.step, .stp) - Mechanical design files
1093
+ - STL (.stl) - 3D polygon meshes
1094
+ - OBJ (.obj) - Geometry and texture
1095
+ - glTF/GLB (.gltf, .glb) - 3D transmission format
1096
+ - IGES (.iges, .igs) - Surface interchange
1097
+ - DXF (.dxf) - AutoCAD drawings
1098
+ - PLY (.ply) - Point clouds and meshes
1099
+ - 3MF (.3mf) - 3D Manufacturing Format
1100
+ - COLLADA (.dae) - Scene format
1101
+
1102
+ Format is auto-detected from file extension.
1103
+ `
1104
+ },
1105
+ {
1106
+ id: 'formats-export',
1107
+ title: 'Export Files',
1108
+ category: 'File Formats',
1109
+ description: 'Save designs to standard formats',
1110
+ shortcut: 'Ctrl+Shift+E',
1111
+ content: `
1112
+ Supported export formats:
1113
+ - STL (ASCII or binary) - 3D printing
1114
+ - OBJ - Universal 3D format
1115
+ - glTF/GLB - Optimized 3D format
1116
+ - DXF - AutoCAD 2D/3D
1117
+ - PDF - Vector drawings
1118
+ - SVG - 2D vector graphics
1119
+ - PLY - Point cloud format
1120
+ - 3MF - 3D Manufacturing Format
1121
+ - JSON - cycleCAD native format
1122
+
1123
+ Export all visible objects or selection.
1124
+ `
1125
+ },
1126
+ {
1127
+ id: 'formats-batch',
1128
+ title: 'Batch Conversion',
1129
+ category: 'File Formats',
1130
+ description: 'Convert multiple files at once',
1131
+ shortcut: 'Ctrl+Shift+B',
1132
+ content: `
1133
+ Convert multiple files to different format:
1134
+ 1. Select multiple files
1135
+ 2. Choose target format
1136
+ 3. Click Convert
1137
+ 4. Files download as ZIP
1138
+
1139
+ Useful for preparing files for 3D printing,
1140
+ CAM, or sharing in specific formats.
1141
+ `
1142
+ },
1143
+ {
1144
+ id: 'formats-detect',
1145
+ title: 'Format Detection',
1146
+ category: 'File Formats',
1147
+ description: 'Automatic file format recognition',
1148
+ shortcut: 'Auto',
1149
+ content: `
1150
+ cycleCAD automatically detects file formats
1151
+ from file extensions and headers.
1152
+
1153
+ If auto-detection fails, you can specify
1154
+ format explicitly in import dialog.
1155
+
1156
+ Supported detection:
1157
+ - Extension-based (.step, .stl, etc)
1158
+ - Header bytes (STL binary vs ASCII)
1159
+ - MIME type information
1160
+ `
1161
+ }
1162
+ ];
1163
+
1164
+ export default {
1165
+ init,
1166
+ detectFormat,
1167
+ getSupportedFormats,
1168
+ import: import_,
1169
+ export: export_,
1170
+ batchConvert,
1171
+ getLastError,
1172
+ helpEntries
1173
+ };