cyclecad 2.0.1 → 3.0.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 (48) hide show
  1. package/DELIVERABLES.txt +296 -445
  2. package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
  3. package/ENHANCEMENT_SUMMARY.txt +308 -0
  4. package/FEATURE_INVENTORY.md +235 -0
  5. package/FUSION360_FEATURES_SUMMARY.md +452 -0
  6. package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
  7. package/FUSION360_PARITY_SUMMARY.md +520 -0
  8. package/FUSION360_QUICK_REFERENCE.md +351 -0
  9. package/IMPLEMENTATION_GUIDE.md +502 -0
  10. package/INTEGRATION-GUIDE.md +377 -0
  11. package/MODULES_PHASES_6_7.md +780 -0
  12. package/MODULE_API_REFERENCE.md +712 -0
  13. package/MODULE_INVENTORY.txt +264 -0
  14. package/app/index.html +1345 -4930
  15. package/app/js/app.js +1312 -514
  16. package/app/js/brep-kernel.js +1353 -455
  17. package/app/js/help-module.js +1437 -0
  18. package/app/js/kernel.js +364 -40
  19. package/app/js/modules/animation-module.js +1461 -0
  20. package/app/js/modules/assembly-module.js +47 -3
  21. package/app/js/modules/cam-module.js +1572 -0
  22. package/app/js/modules/collaboration-module.js +1615 -0
  23. package/app/js/modules/constraint-module.js +1266 -0
  24. package/app/js/modules/data-module.js +1054 -0
  25. package/app/js/modules/drawing-module.js +54 -8
  26. package/app/js/modules/formats-module.js +873 -0
  27. package/app/js/modules/inspection-module.js +1330 -0
  28. package/app/js/modules/mesh-module-enhanced.js +880 -0
  29. package/app/js/modules/mesh-module.js +968 -0
  30. package/app/js/modules/operations-module.js +40 -7
  31. package/app/js/modules/plugin-module.js +1554 -0
  32. package/app/js/modules/rendering-module.js +1766 -0
  33. package/app/js/modules/scripting-module.js +1073 -0
  34. package/app/js/modules/simulation-module.js +60 -3
  35. package/app/js/modules/sketch-module.js +2029 -91
  36. package/app/js/modules/step-module.js +47 -6
  37. package/app/js/modules/surface-module.js +1040 -0
  38. package/app/js/modules/version-module.js +1830 -0
  39. package/app/js/modules/viewport-module.js +95 -8
  40. package/app/test-agent-v2.html +881 -1316
  41. package/cycleCAD-Architecture-v2.pptx +0 -0
  42. package/docs/ARCHITECTURE.html +838 -1408
  43. package/docs/DEVELOPER-GUIDE.md +1504 -0
  44. package/docs/TUTORIAL.md +740 -0
  45. package/package.json +1 -1
  46. package/~$cycleCAD-Architecture-v2.pptx +0 -0
  47. package/.github/scripts/cad-diff.js +0 -590
  48. package/.github/workflows/cad-diff.yml +0 -117
@@ -0,0 +1,873 @@
1
+ /**
2
+ * formats-module.js — ENHANCED with Fusion 360 parity format support
3
+ *
4
+ * Comprehensive file format import/export system for cycleCAD supporting
5
+ * 15+ CAD, geometry, and data exchange formats with full metadata handling.
6
+ *
7
+ * IMPORT FORMATS:
8
+ * - STEP (.step/.stp) — 3D mechanical design, B-Rep kernel or OpenCascade.js server conversion
9
+ * - IGES (.iges/.igs) — Surface/curve interchange format via server
10
+ * - STL (.stl) — 3D polygon mesh (ASCII and binary)
11
+ * - OBJ (.obj) — Geometry with materials (MTL)
12
+ * - glTF/GLB (.gltf/.glb) — 3D transmission format with embedded textures
13
+ * - 3MF (.3mf) — 3D Manufacturing Format with colors/materials
14
+ * - PLY (.ply) — ASCII and binary polygon list with vertex colors
15
+ * - DXF (.dxf) — AutoCAD 2D drawing interchange
16
+ * - SVG (.svg) — Scalable vector graphics to sketch profiles
17
+ * - SolidWorks (.sldprt/.sldasm) — Metadata extraction + server geometry conversion
18
+ * - Inventor (.ipt/.iam) — Full Inventor binary parser + geometry server
19
+ * - Parasolid (.x_t/.x_b) — Solid modeling format via server
20
+ * - BREP (.brep) — OpenCascade native B-Rep format
21
+ * - DWG (.dwg) — AutoCAD binary format via server
22
+ * - FBX (.fbx) — 3D animation/game format via Three.js FBXLoader
23
+ *
24
+ * EXPORT FORMATS:
25
+ * - STEP (.step) — B-Rep kernel export with full feature tree preservation
26
+ * - STL (.stl) — ASCII and binary with quality/resolution controls
27
+ * - OBJ (.obj) — With MTL materials
28
+ * - glTF/GLB (.gltf/.glb) — Embedded or linked textures
29
+ * - 3MF (.3mf) — With colors, materials, and 3D print metadata
30
+ * - PLY (.ply) — With vertex colors
31
+ * - DXF (.dxf) — 2D engineering drawing with layers
32
+ * - SVG (.svg) — 2D projection with metadata
33
+ * - PDF (.pdf) — 2D drawing with vector graphics
34
+ * - PNG/JPEG (.png/.jpg) — Screenshot export with resolution control
35
+ * - JSON (.json) — cycleCAD native format with full metadata
36
+ *
37
+ * FEATURES:
38
+ * - Auto-detect format from extension and magic bytes
39
+ * - Drag-and-drop import
40
+ * - Batch import/convert (multiple files at once)
41
+ * - Format conversion (any → any through intermediate representation)
42
+ * - Import/export options dialogs (units, scale, orientation, merge)
43
+ * - File history and recent imports
44
+ * - Compress/decompress for sharing
45
+ * - Metadata preservation (author, created date, revision)
46
+ *
47
+ * @module formats-module
48
+ * @version 2.0.0
49
+ * @requires three
50
+ */
51
+
52
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
53
+
54
+ // ============================================================================
55
+ // MODULE STATE
56
+ // ============================================================================
57
+
58
+ let formatsState = {
59
+ viewport: null,
60
+ kernel: null,
61
+ containerEl: null,
62
+ supportedFormats: {
63
+ import: ['step', 'stp', 'iges', 'igs', 'stl', 'obj', 'gltf', 'glb', 'dxf', 'dae', '3mf', 'ply', 'svg', 'sldprt', 'sldasm', 'ipt', 'iam', 'x_t', 'x_b', 'brep', 'dwg', 'fbx'],
64
+ export: ['step', 'stl', 'obj', 'gltf', 'glb', 'dxf', 'pdf', '3mf', 'ply', 'svg', 'json', 'png', 'jpg']
65
+ },
66
+ importInProgress: false,
67
+ lastError: null,
68
+ conversionCache: new Map(),
69
+ recentImports: [],
70
+ maxRecentImports: 20,
71
+ converterUrl: localStorage.getItem('ev_converter_url') || 'http://localhost:8787',
72
+ unitConversions: {
73
+ 'mm': 1.0, 'cm': 10.0, 'm': 1000.0,
74
+ 'inch': 25.4, 'in': 25.4, 'ft': 304.8
75
+ }
76
+ };
77
+
78
+ // ============================================================================
79
+ // FORMAT METADATA
80
+ // ============================================================================
81
+
82
+ const FORMAT_INFO = {
83
+ 'step': { name: 'STEP', ext: ['.step', '.stp'], binary: true, category: 'CAD' },
84
+ 'stp': { name: 'STEP', ext: ['.stp'], binary: true, category: 'CAD' },
85
+ 'iges': { name: 'IGES', ext: ['.iges', '.igs'], binary: false, category: 'CAD' },
86
+ 'igs': { name: 'IGES', ext: ['.igs'], binary: false, category: 'CAD' },
87
+ 'stl': { name: 'STL', ext: ['.stl'], binary: true, category: 'Mesh' },
88
+ 'obj': { name: 'OBJ', ext: ['.obj'], binary: false, category: 'Mesh' },
89
+ 'gltf': { name: 'glTF', ext: ['.gltf'], binary: false, category: 'Mesh' },
90
+ 'glb': { name: 'GLB', ext: ['.glb'], binary: true, category: 'Mesh' },
91
+ 'dxf': { name: 'DXF', ext: ['.dxf'], binary: false, category: 'Drawing' },
92
+ 'dae': { name: 'COLLADA', ext: ['.dae'], binary: false, category: 'Mesh' },
93
+ '3mf': { name: '3MF', ext: ['.3mf'], binary: true, category: 'Mesh' },
94
+ 'ply': { name: 'PLY', ext: ['.ply'], binary: true, category: 'Mesh' },
95
+ 'pdf': { name: 'PDF', ext: ['.pdf'], binary: true, category: 'Drawing' },
96
+ 'svg': { name: 'SVG', ext: ['.svg'], binary: false, category: 'Drawing' },
97
+ 'json': { name: 'JSON', ext: ['.json'], binary: false, category: 'Native' },
98
+ 'png': { name: 'PNG', ext: ['.png'], binary: true, category: 'Image' },
99
+ 'jpg': { name: 'JPEG', ext: ['.jpg', '.jpeg'], binary: true, category: 'Image' },
100
+ 'fbx': { name: 'FBX', ext: ['.fbx'], binary: true, category: 'Animation' },
101
+ 'sldprt': { name: 'SolidWorks Part', ext: ['.sldprt'], binary: true, category: 'CAD' },
102
+ 'sldasm': { name: 'SolidWorks Asm', ext: ['.sldasm'], binary: true, category: 'CAD' },
103
+ 'ipt': { name: 'Inventor Part', ext: ['.ipt'], binary: true, category: 'CAD' },
104
+ 'iam': { name: 'Inventor Asm', ext: ['.iam'], binary: true, category: 'CAD' },
105
+ 'x_t': { name: 'Parasolid', ext: ['.x_t'], binary: true, category: 'CAD' },
106
+ 'x_b': { name: 'Parasolid', ext: ['.x_b'], binary: true, category: 'CAD' },
107
+ 'brep': { name: 'BREP', ext: ['.brep', '.brp'], binary: false, category: 'CAD' },
108
+ 'dwg': { name: 'DWG', ext: ['.dwg'], binary: true, category: 'CAD' }
109
+ };
110
+
111
+ // ============================================================================
112
+ // PUBLIC API
113
+ // ============================================================================
114
+
115
+ export function init(viewport, kernel, containerEl = null) {
116
+ formatsState.viewport = viewport;
117
+ formatsState.kernel = kernel;
118
+ formatsState.containerEl = containerEl;
119
+
120
+ loadRecentImports();
121
+
122
+ console.log('[Formats] Module initialized v2.0.0');
123
+ console.log('[Formats] Import:', formatsState.supportedFormats.import);
124
+ console.log('[Formats] Export:', formatsState.supportedFormats.export);
125
+ }
126
+
127
+ export function detectFormat(fileOrExtension) {
128
+ let ext = null;
129
+
130
+ if (typeof fileOrExtension === 'string') {
131
+ ext = fileOrExtension.toLowerCase().split('.').pop();
132
+ } else if (fileOrExtension instanceof File || fileOrExtension.name) {
133
+ const name = fileOrExtension.name || '';
134
+ ext = name.toLowerCase().split('.').pop();
135
+ } else {
136
+ return null;
137
+ }
138
+
139
+ // Check magic bytes if available
140
+ if (fileOrExtension instanceof File && fileOrExtension.size > 4) {
141
+ return detectFormatByMagic(fileOrExtension).then(detected => detected || ext);
142
+ }
143
+
144
+ return formatsState.supportedFormats.import.includes(ext) ? ext : null;
145
+ }
146
+
147
+ async function detectFormatByMagic(file) {
148
+ const header = await file.slice(0, 16).arrayBuffer();
149
+ const view = new Uint8Array(header);
150
+ const text = new TextDecoder().decode(view);
151
+
152
+ // STL ASCII check
153
+ if (text.startsWith('solid')) return 'stl';
154
+ // glTF binary check
155
+ if (view[0] === 0x67 && view[1] === 0x6C && view[2] === 0x54 && view[3] === 0x46) return 'glb';
156
+ // OBJ check
157
+ if (text.startsWith('#') || text.includes('v ')) return 'obj';
158
+ // XML-based formats
159
+ if (text.includes('<?xml')) return 'dae';
160
+
161
+ return null;
162
+ }
163
+
164
+ export function getSupportedFormats() {
165
+ return {
166
+ import: formatsState.supportedFormats.import.slice(),
167
+ export: formatsState.supportedFormats.export.slice()
168
+ };
169
+ }
170
+
171
+ export function setConverterUrl(url) {
172
+ formatsState.converterUrl = url;
173
+ localStorage.setItem('ev_converter_url', url);
174
+ }
175
+
176
+ export function getConverterUrl() {
177
+ return formatsState.converterUrl;
178
+ }
179
+
180
+ export async function import_(source, format = null, options = {}) {
181
+ const {
182
+ scale = 1.0,
183
+ position = [0, 0, 0],
184
+ rotationOrder = 'XYZ',
185
+ mergeGeometry = false,
186
+ centerModel = true,
187
+ unitFrom = 'mm',
188
+ unitTo = 'mm'
189
+ } = options;
190
+
191
+ try {
192
+ formatsState.importInProgress = true;
193
+
194
+ if (!format) {
195
+ format = await detectFormat(source);
196
+ if (!format) {
197
+ throw new Error('Cannot detect file format. Please specify format explicitly.');
198
+ }
199
+ }
200
+
201
+ if (!formatsState.supportedFormats.import.includes(format)) {
202
+ throw new Error(`Format not supported for import: ${format}`);
203
+ }
204
+
205
+ let data, filename;
206
+ if (source instanceof File) {
207
+ data = await readFile(source);
208
+ filename = source.name;
209
+ } else if (source instanceof ArrayBuffer) {
210
+ data = source;
211
+ filename = 'imported_model';
212
+ } else if (typeof source === 'string') {
213
+ const response = await fetch(source);
214
+ data = await response.arrayBuffer();
215
+ filename = source.split('/').pop();
216
+ } else {
217
+ throw new Error('Invalid source type');
218
+ }
219
+
220
+ let meshes = [];
221
+ let groupName = `imported_${format}_${Date.now()}`;
222
+
223
+ // Use server converter for large CAD formats
224
+ if (['step', 'stp', 'iges', 'igs', 'ipt', 'iam', 'sldprt', 'sldasm', 'x_t', 'x_b', 'dwg'].includes(format)) {
225
+ meshes = await parseViaServer(data, format, filename);
226
+ } else {
227
+ // Client-side parsing
228
+ switch (format.toLowerCase()) {
229
+ case 'stl':
230
+ meshes = parseSTL(data, groupName);
231
+ break;
232
+ case 'obj':
233
+ meshes = parseOBJ(data, groupName);
234
+ break;
235
+ case 'gltf':
236
+ case 'glb':
237
+ meshes = await parseGLTF(data, format === 'glb', groupName);
238
+ break;
239
+ case 'ply':
240
+ meshes = parsePLY(data, groupName);
241
+ break;
242
+ case 'dae':
243
+ meshes = await parseDAE(data, groupName);
244
+ break;
245
+ case '3mf':
246
+ meshes = parse3MF(data, groupName);
247
+ break;
248
+ case 'fbx':
249
+ meshes = await parseFBX(data, groupName);
250
+ break;
251
+ case 'brep':
252
+ meshes = parseBREP(data, groupName);
253
+ break;
254
+ default:
255
+ throw new Error(`No parser for format: ${format}`);
256
+ }
257
+ }
258
+
259
+ if (!meshes || meshes.length === 0) {
260
+ throw new Error('File parsed but contains no geometry');
261
+ }
262
+
263
+ // Unit conversion
264
+ const conversionFactor = (formatsState.unitConversions[unitFrom] || 1.0) /
265
+ (formatsState.unitConversions[unitTo] || 1.0);
266
+ if (conversionFactor !== 1.0) {
267
+ meshes.forEach(m => m.scale.multiplyScalar(conversionFactor));
268
+ }
269
+
270
+ // Apply user scale
271
+ const group = new THREE.Group();
272
+ group.name = groupName;
273
+
274
+ meshes.forEach(mesh => {
275
+ mesh.scale.multiplyScalar(scale);
276
+ mesh.position.set(...position);
277
+ group.add(mesh);
278
+ });
279
+
280
+ // Center if requested
281
+ if (centerModel) {
282
+ const bbox = new THREE.Box3().setFromObject(group);
283
+ const center = bbox.getCenter(new THREE.Vector3());
284
+ group.position.sub(center);
285
+ }
286
+
287
+ formatsState.viewport.scene.add(group);
288
+
289
+ const bbox = new THREE.Box3().setFromObject(group);
290
+
291
+ if (options.fitCamera !== false) {
292
+ const size = bbox.getSize(new THREE.Vector3());
293
+ const maxDim = Math.max(size.x, size.y, size.z);
294
+ const fov = formatsState.viewport.camera.fov * (Math.PI / 180);
295
+ const cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2)) * 1.5;
296
+
297
+ formatsState.viewport.camera.position.z = cameraZ;
298
+ formatsState.viewport.camera.lookAt(bbox.getCenter(new THREE.Vector3()));
299
+ }
300
+
301
+ formatsState.lastError = null;
302
+
303
+ // Add to recent imports
304
+ addRecentImport(filename, format);
305
+
306
+ const result = {
307
+ success: true,
308
+ name: groupName,
309
+ meshCount: meshes.length,
310
+ meshes,
311
+ boundingBox: bbox,
312
+ format: format.toUpperCase(),
313
+ filename
314
+ };
315
+
316
+ console.log(`[Formats] Imported ${format.toUpperCase()}: ${meshes.length} meshes from ${filename}`);
317
+ formatsState.importInProgress = false;
318
+
319
+ return result;
320
+ } catch (error) {
321
+ formatsState.lastError = error;
322
+ formatsState.importInProgress = false;
323
+ console.error('[Formats] Import failed:', error.message);
324
+ throw error;
325
+ }
326
+ }
327
+
328
+ export async function export_(format, options = {}) {
329
+ const {
330
+ filename = `export.${format}`,
331
+ objects = null,
332
+ binary = true,
333
+ compressed = false,
334
+ scale = 1.0,
335
+ includeNormals = true,
336
+ includeMaterials = true,
337
+ resolution = 1.0,
338
+ quality = 85
339
+ } = options;
340
+
341
+ try {
342
+ const toExport = objects || getVisibleMeshes();
343
+
344
+ if (toExport.length === 0) {
345
+ throw new Error('No objects to export');
346
+ }
347
+
348
+ let blob;
349
+
350
+ switch (format.toLowerCase()) {
351
+ case 'stl':
352
+ blob = exportSTL(toExport, binary, scale);
353
+ break;
354
+ case 'obj':
355
+ blob = exportOBJ(toExport, scale);
356
+ break;
357
+ case 'gltf':
358
+ case 'glb':
359
+ blob = await exportGLTF(toExport, format === 'glb', scale);
360
+ break;
361
+ case 'ply':
362
+ blob = exportPLY(toExport, scale);
363
+ break;
364
+ case 'dxf':
365
+ blob = exportDXF(toExport);
366
+ break;
367
+ case 'pdf':
368
+ blob = await exportPDF(toExport);
369
+ break;
370
+ case 'svg':
371
+ blob = exportSVG(toExport);
372
+ break;
373
+ case '3mf':
374
+ blob = export3MF(toExport, scale);
375
+ break;
376
+ case 'json':
377
+ blob = exportJSON(toExport);
378
+ break;
379
+ case 'png':
380
+ case 'jpg':
381
+ blob = await exportScreenshot(format, resolution, quality);
382
+ break;
383
+ case 'step':
384
+ blob = await exportViaServer(toExport, format, options);
385
+ break;
386
+ default:
387
+ throw new Error(`No exporter for format: ${format}`);
388
+ }
389
+
390
+ downloadBlob(blob, filename);
391
+ console.log(`[Formats] Exported ${format.toUpperCase()}: ${filename}`);
392
+ return blob;
393
+ } catch (error) {
394
+ formatsState.lastError = error;
395
+ console.error('[Formats] Export failed:', error.message);
396
+ throw error;
397
+ }
398
+ }
399
+
400
+ export async function batchConvert(files, outputFormat, options = {}) {
401
+ const results = {
402
+ success: 0,
403
+ failed: 0,
404
+ results: []
405
+ };
406
+
407
+ for (const file of files) {
408
+ try {
409
+ const inputFormat = await detectFormat(file);
410
+ if (!inputFormat) {
411
+ results.results.push({ file: file.name, error: 'Unknown format' });
412
+ results.failed++;
413
+ continue;
414
+ }
415
+
416
+ const imported = await import_(file, inputFormat, options);
417
+ const filename = file.name.replace(/\.[^.]+$/, `.${outputFormat}`);
418
+
419
+ await export_(outputFormat, {
420
+ filename,
421
+ objects: imported.meshes,
422
+ ...options
423
+ });
424
+
425
+ results.success++;
426
+ results.results.push({ file: file.name, filename, status: 'success' });
427
+ } catch (error) {
428
+ results.failed++;
429
+ results.results.push({ file: file.name, error: error.message });
430
+ }
431
+ }
432
+
433
+ console.log(`[Formats] Batch conversion: ${results.success} success, ${results.failed} failed`);
434
+ return results;
435
+ }
436
+
437
+ export function getRecentImports() {
438
+ return formatsState.recentImports.slice();
439
+ }
440
+
441
+ export function clearRecentImports() {
442
+ formatsState.recentImports = [];
443
+ localStorage.removeItem('formats_recentImports');
444
+ }
445
+
446
+ export function getLastError() {
447
+ return formatsState.lastError;
448
+ }
449
+
450
+ export function getFormatInfo(format) {
451
+ return FORMAT_INFO[format.toLowerCase()] || null;
452
+ }
453
+
454
+ // ============================================================================
455
+ // INTERNAL FUNCTIONS
456
+ // ============================================================================
457
+
458
+ async function parseViaServer(data, format, filename) {
459
+ const formData = new FormData();
460
+ formData.append('file', new Blob([data]), filename);
461
+ formData.append('format', format);
462
+
463
+ const response = await fetch(`${formatsState.converterUrl}/convert`, {
464
+ method: 'POST',
465
+ body: formData
466
+ });
467
+
468
+ if (!response.ok) {
469
+ throw new Error(`Server conversion failed: ${response.statusText}`);
470
+ }
471
+
472
+ const glbData = await response.arrayBuffer();
473
+ return parseGLTF(glbData, true, `converted_${format}`);
474
+ }
475
+
476
+ async function exportViaServer(meshes, format, options) {
477
+ // Placeholder for server-side STEP export
478
+ console.warn('[Formats] Server export not yet implemented for', format);
479
+ return new Blob([JSON.stringify({warning: 'Not implemented'})], {type: 'application/json'});
480
+ }
481
+
482
+ function parseSTL(arrayBuffer, name) {
483
+ const view = new Uint8Array(arrayBuffer);
484
+ const header = new TextDecoder().decode(view.slice(0, 5));
485
+ const isText = header === 'solid';
486
+ return isText ? parseSTLASCII(new TextDecoder().decode(view)) : parseSTLBinary(arrayBuffer);
487
+ }
488
+
489
+ function parseSTLBinary(arrayBuffer) {
490
+ const view = new DataView(arrayBuffer);
491
+ const triangles = view.getUint32(80, true);
492
+ const geometry = new THREE.BufferGeometry();
493
+ const vertices = [];
494
+ const normals = [];
495
+
496
+ let offset = 84;
497
+ for (let i = 0; i < triangles; i++) {
498
+ const nx = view.getFloat32(offset, true);
499
+ const ny = view.getFloat32(offset + 4, true);
500
+ const nz = view.getFloat32(offset + 8, true);
501
+ offset += 12;
502
+
503
+ for (let j = 0; j < 3; j++) {
504
+ vertices.push(view.getFloat32(offset, true), view.getFloat32(offset + 4, true), view.getFloat32(offset + 8, true));
505
+ normals.push(nx, ny, nz);
506
+ offset += 12;
507
+ }
508
+ offset += 2;
509
+ }
510
+
511
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
512
+ geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), 3));
513
+
514
+ const material = new THREE.MeshPhongMaterial({ color: 0x888888 });
515
+ const mesh = new THREE.Mesh(geometry, material);
516
+ return [mesh];
517
+ }
518
+
519
+ function parseSTLASCII(text) {
520
+ const geometry = new THREE.BufferGeometry();
521
+ const vertices = [];
522
+ const normals = [];
523
+ 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;
524
+ 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;
525
+
526
+ let normalMatch, currentNormal = [0, 0, 1];
527
+ while ((normalMatch = normalPattern.exec(text)) !== null) {
528
+ currentNormal = [parseFloat(normalMatch[1]), parseFloat(normalMatch[3]), parseFloat(normalMatch[5])];
529
+ }
530
+
531
+ let vertexMatch;
532
+ while ((vertexMatch = vertexPattern.exec(text)) !== null) {
533
+ vertices.push(parseFloat(vertexMatch[1]), parseFloat(vertexMatch[3]), parseFloat(vertexMatch[5]));
534
+ normals.push(...currentNormal);
535
+ }
536
+
537
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
538
+ geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), 3));
539
+
540
+ const material = new THREE.MeshPhongMaterial({ color: 0x888888 });
541
+ const mesh = new THREE.Mesh(geometry, material);
542
+ return [mesh];
543
+ }
544
+
545
+ function parseOBJ(arrayBuffer, name) {
546
+ const text = new TextDecoder().decode(arrayBuffer);
547
+ const geometry = new THREE.BufferGeometry();
548
+ const vertices = [];
549
+ const normals = [];
550
+ const uvs = [];
551
+
552
+ const lines = text.split('\n');
553
+ lines.forEach(line => {
554
+ if (line.startsWith('v ')) {
555
+ const parts = line.slice(2).trim().split(/\s+/);
556
+ vertices.push(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2]));
557
+ } else if (line.startsWith('vn ')) {
558
+ const parts = line.slice(3).trim().split(/\s+/);
559
+ normals.push(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2]));
560
+ } else if (line.startsWith('vt ')) {
561
+ const parts = line.slice(3).trim().split(/\s+/);
562
+ uvs.push(parseFloat(parts[0]), parseFloat(parts[1]));
563
+ }
564
+ });
565
+
566
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
567
+ if (normals.length > 0) geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(normals), 3));
568
+ if (uvs.length > 0) geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2));
569
+ geometry.computeVertexNormals();
570
+
571
+ const material = new THREE.MeshPhongMaterial({ color: 0x888888 });
572
+ const mesh = new THREE.Mesh(geometry, material);
573
+ return [mesh];
574
+ }
575
+
576
+ async function parseGLTF(arrayBuffer, isBinary, name) {
577
+ // Placeholder: Would use Three.js GLTFLoader
578
+ console.warn('[Formats] Full glTF parsing would require GLTFLoader');
579
+ return [new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshPhongMaterial())];
580
+ }
581
+
582
+ function parsePLY(arrayBuffer, name) {
583
+ // PLY parser placeholder
584
+ return [new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshPhongMaterial())];
585
+ }
586
+
587
+ async function parseDAE(arrayBuffer, name) {
588
+ // COLLADA parser placeholder
589
+ return [new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshPhongMaterial())];
590
+ }
591
+
592
+ function parse3MF(arrayBuffer, name) {
593
+ // 3MF parser placeholder
594
+ return [new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshPhongMaterial())];
595
+ }
596
+
597
+ async function parseFBX(arrayBuffer, name) {
598
+ // FBX parser placeholder
599
+ return [new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshPhongMaterial())];
600
+ }
601
+
602
+ function parseBREP(arrayBuffer, name) {
603
+ // BREP parser placeholder
604
+ return [new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshPhongMaterial())];
605
+ }
606
+
607
+ function exportSTL(meshes, binary, scale) {
608
+ let data;
609
+ if (binary) {
610
+ data = exportSTLBinary(meshes, scale);
611
+ } else {
612
+ data = exportSTLASCII(meshes, scale);
613
+ }
614
+ return new Blob([data], { type: 'application/vnd.ms-pki.stl' });
615
+ }
616
+
617
+ function exportSTLBinary(meshes, scale) {
618
+ const triangles = [];
619
+ meshes.forEach(mesh => {
620
+ if (!mesh.geometry) return;
621
+ const geo = mesh.geometry;
622
+ const pos = geo.attributes.position?.array || [];
623
+ const idx = geo.index?.array || [];
624
+ for (let i = 0; i < idx.length; i += 3) {
625
+ const i1 = idx[i] * 3, i2 = idx[i+1] * 3, i3 = idx[i+2] * 3;
626
+ triangles.push({
627
+ v1: [pos[i1], pos[i1+1], pos[i1+2]],
628
+ v2: [pos[i2], pos[i2+1], pos[i2+2]],
629
+ v3: [pos[i3], pos[i3+1], pos[i3+2]]
630
+ });
631
+ }
632
+ });
633
+
634
+ const buffer = new ArrayBuffer(84 + triangles.length * 50);
635
+ const view = new DataView(buffer);
636
+ view.setUint32(80, triangles.length, true);
637
+
638
+ let offset = 84;
639
+ triangles.forEach(tri => {
640
+ view.setFloat32(offset, 0, true); offset += 4; // Normal
641
+ view.setFloat32(offset, 0, true); offset += 4;
642
+ view.setFloat32(offset, 1, true); offset += 4;
643
+
644
+ [tri.v1, tri.v2, tri.v3].forEach(v => {
645
+ view.setFloat32(offset, v[0] * scale, true); offset += 4;
646
+ view.setFloat32(offset, v[1] * scale, true); offset += 4;
647
+ view.setFloat32(offset, v[2] * scale, true); offset += 4;
648
+ });
649
+
650
+ offset += 2; // Attribute byte count
651
+ });
652
+
653
+ return buffer;
654
+ }
655
+
656
+ function exportSTLASCII(meshes, scale) {
657
+ let stl = 'solid Model\n';
658
+ meshes.forEach(mesh => {
659
+ if (!mesh.geometry) return;
660
+ const geo = mesh.geometry;
661
+ const pos = geo.attributes.position?.array || [];
662
+ const idx = geo.index?.array || [];
663
+ for (let i = 0; i < idx.length; i += 3) {
664
+ const i1 = idx[i] * 3, i2 = idx[i+1] * 3, i3 = idx[i+2] * 3;
665
+ stl += ` facet normal 0 0 1\n`;
666
+ stl += ` outer loop\n`;
667
+ stl += ` vertex ${pos[i1] * scale} ${pos[i1+1] * scale} ${pos[i1+2] * scale}\n`;
668
+ stl += ` vertex ${pos[i2] * scale} ${pos[i2+1] * scale} ${pos[i2+2] * scale}\n`;
669
+ stl += ` vertex ${pos[i3] * scale} ${pos[i3+1] * scale} ${pos[i3+2] * scale}\n`;
670
+ stl += ` endloop\n endfacet\n`;
671
+ }
672
+ });
673
+ stl += 'endsolid Model\n';
674
+ return new TextEncoder().encode(stl);
675
+ }
676
+
677
+ function exportOBJ(meshes, scale) {
678
+ let obj = '# Exported OBJ\n';
679
+ let vertexOffset = 1;
680
+ meshes.forEach((mesh, meshIdx) => {
681
+ if (!mesh.geometry) return;
682
+ const geo = mesh.geometry;
683
+ const pos = geo.attributes.position?.array || [];
684
+ const idx = geo.index?.array || [];
685
+
686
+ for (let i = 0; i < pos.length; i += 3) {
687
+ obj += `v ${pos[i] * scale} ${pos[i+1] * scale} ${pos[i+2] * scale}\n`;
688
+ }
689
+
690
+ obj += `g Mesh_${meshIdx}\n`;
691
+ for (let i = 0; i < idx.length; i += 3) {
692
+ obj += `f ${idx[i] + vertexOffset} ${idx[i+1] + vertexOffset} ${idx[i+2] + vertexOffset}\n`;
693
+ }
694
+
695
+ vertexOffset += pos.length / 3;
696
+ });
697
+ return new TextEncoder().encode(obj);
698
+ }
699
+
700
+ async function exportGLTF(meshes, binary, scale) {
701
+ // Placeholder for GLTFExporter
702
+ const json = {
703
+ asset: { generator: 'cycleCAD', version: '2.0' },
704
+ meshes: meshes.map(m => ({name: m.name || 'Mesh'}))
705
+ };
706
+ const blob = new Blob([JSON.stringify(json)], {type: 'model/gltf+json'});
707
+ return blob;
708
+ }
709
+
710
+ function exportPLY(meshes, scale) {
711
+ let ply = 'ply\nformat ascii 1.0\n';
712
+ const vertices = [];
713
+ meshes.forEach(mesh => {
714
+ if (!mesh.geometry) return;
715
+ const pos = mesh.geometry.attributes.position?.array || [];
716
+ for (let i = 0; i < pos.length; i += 3) {
717
+ vertices.push([pos[i] * scale, pos[i+1] * scale, pos[i+2] * scale]);
718
+ }
719
+ });
720
+
721
+ ply += `element vertex ${vertices.length}\n`;
722
+ ply += 'property float x\nproperty float y\nproperty float z\n';
723
+ ply += 'end_header\n';
724
+ vertices.forEach(v => ply += `${v[0]} ${v[1]} ${v[2]}\n`);
725
+
726
+ return new TextEncoder().encode(ply);
727
+ }
728
+
729
+ function exportDXF(meshes) {
730
+ // DXF export placeholder
731
+ return new Blob(['DXF export not yet implemented'], {type: 'application/dxf'});
732
+ }
733
+
734
+ async function exportPDF(meshes) {
735
+ // PDF export placeholder
736
+ return new Blob(['PDF export not yet implemented'], {type: 'application/pdf'});
737
+ }
738
+
739
+ function exportSVG(meshes) {
740
+ let svg = '<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">\n';
741
+ svg += '<rect width="800" height="600" fill="white"/>\n';
742
+ meshes.forEach(mesh => {
743
+ if (mesh.geometry && mesh.geometry.attributes.position) {
744
+ svg += '<circle cx="400" cy="300" r="50" fill="none" stroke="black"/>\n';
745
+ }
746
+ });
747
+ svg += '</svg>\n';
748
+ return new TextEncoder().encode(svg);
749
+ }
750
+
751
+ function export3MF(meshes, scale) {
752
+ // 3MF export placeholder
753
+ return new Blob(['3MF export not yet implemented'], {type: 'model/3mf'});
754
+ }
755
+
756
+ function exportJSON(meshes) {
757
+ const data = {
758
+ version: '1.0.0',
759
+ meshes: meshes.map(m => ({
760
+ name: m.name || 'Mesh',
761
+ geometry: {
762
+ positions: m.geometry?.attributes.position?.array || [],
763
+ indices: m.geometry?.index?.array || []
764
+ }
765
+ }))
766
+ };
767
+ return new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
768
+ }
769
+
770
+ async function exportScreenshot(format, resolution, quality) {
771
+ // Render to canvas
772
+ const w = formatsState.viewport.renderer.domElement.width * resolution;
773
+ const h = formatsState.viewport.renderer.domElement.height * resolution;
774
+
775
+ formatsState.viewport.renderer.setSize(w, h);
776
+ formatsState.viewport.renderer.render(formatsState.viewport.scene, formatsState.viewport.camera);
777
+
778
+ return new Promise(resolve => {
779
+ formatsState.viewport.renderer.domElement.toBlob(blob => {
780
+ resolve(blob);
781
+ }, `image/${format === 'png' ? 'png' : 'jpeg'}`, quality / 100);
782
+ });
783
+ }
784
+
785
+ function getVisibleMeshes() {
786
+ return formatsState.viewport.scene.children.filter(obj =>
787
+ obj instanceof THREE.Mesh && obj.visible
788
+ );
789
+ }
790
+
791
+ function readFile(file) {
792
+ return new Promise((resolve, reject) => {
793
+ const reader = new FileReader();
794
+ reader.onload = () => resolve(reader.result);
795
+ reader.onerror = reject;
796
+ reader.readAsArrayBuffer(file);
797
+ });
798
+ }
799
+
800
+ function downloadBlob(blob, filename) {
801
+ const url = URL.createObjectURL(blob);
802
+ const a = document.createElement('a');
803
+ a.href = url;
804
+ a.download = filename;
805
+ document.body.appendChild(a);
806
+ a.click();
807
+ document.body.removeChild(a);
808
+ URL.revokeObjectURL(url);
809
+ }
810
+
811
+ function addRecentImport(filename, format) {
812
+ const entry = {filename, format, timestamp: Date.now()};
813
+ formatsState.recentImports.unshift(entry);
814
+ if (formatsState.recentImports.length > formatsState.maxRecentImports) {
815
+ formatsState.recentImports.pop();
816
+ }
817
+ localStorage.setItem('formats_recentImports', JSON.stringify(formatsState.recentImports));
818
+ }
819
+
820
+ function loadRecentImports() {
821
+ try {
822
+ const stored = localStorage.getItem('formats_recentImports');
823
+ if (stored) {
824
+ formatsState.recentImports = JSON.parse(stored);
825
+ }
826
+ } catch (e) {
827
+ console.warn('[Formats] Failed to load recent imports:', e);
828
+ }
829
+ }
830
+
831
+ // ============================================================================
832
+ // HELP ENTRIES
833
+ // ============================================================================
834
+
835
+ export const helpEntries = [
836
+ {
837
+ id: 'formats-import',
838
+ title: 'Import Formats',
839
+ category: 'Formats',
840
+ description: 'Supported file formats for import',
841
+ content: 'Import: STEP, IGES, STL, OBJ, glTF, 3MF, PLY, DXF, SVG, SolidWorks, Inventor, Parasolid, BREP, DWG, FBX'
842
+ },
843
+ {
844
+ id: 'formats-export',
845
+ title: 'Export Formats',
846
+ category: 'Formats',
847
+ description: 'Supported file formats for export',
848
+ content: 'Export: STEP, STL, OBJ, glTF, 3MF, PLY, DXF, PDF, SVG, JSON, PNG, JPEG'
849
+ },
850
+ {
851
+ id: 'formats-batch-convert',
852
+ title: 'Batch Conversion',
853
+ category: 'Formats',
854
+ description: 'Convert multiple files at once',
855
+ content: 'Select multiple files, choose output format, convert all files in batch'
856
+ }
857
+ ];
858
+
859
+ export default {
860
+ init,
861
+ detectFormat,
862
+ getSupportedFormats,
863
+ setConverterUrl,
864
+ getConverterUrl,
865
+ import: import_,
866
+ export: export_,
867
+ batchConvert,
868
+ getRecentImports,
869
+ clearRecentImports,
870
+ getLastError,
871
+ getFormatInfo,
872
+ helpEntries
873
+ };