cyclecad 2.1.0 → 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.
@@ -1,87 +1,52 @@
1
1
  /**
2
- * formats-module.js
2
+ * formats-module.js — ENHANCED with Fusion 360 parity format support
3
3
  *
4
4
  * Comprehensive file format import/export system for cycleCAD supporting
5
- * multiple CAD, geometry, and data exchange formats.
5
+ * 15+ CAD, geometry, and data exchange formats with full metadata handling.
6
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
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
19
23
  *
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)
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)
31
46
  *
32
47
  * @module formats-module
33
- * @version 1.0.0
48
+ * @version 2.0.0
34
49
  * @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
50
  */
86
51
 
87
52
  import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
@@ -95,100 +60,107 @@ let formatsState = {
95
60
  kernel: null,
96
61
  containerEl: null,
97
62
  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']
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']
100
65
  },
101
66
  importInProgress: false,
102
67
  lastError: null,
103
- conversionCache: new Map()
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
+ }
104
76
  };
105
77
 
106
78
  // ============================================================================
107
- // MIME TYPE AND EXTENSION MAPPINGS
79
+ // FORMAT METADATA
108
80
  // ============================================================================
109
81
 
110
82
  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 }
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' }
126
109
  };
127
110
 
128
111
  // ============================================================================
129
112
  // PUBLIC API
130
113
  // ============================================================================
131
114
 
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
115
  export function init(viewport, kernel, containerEl = null) {
140
116
  formatsState.viewport = viewport;
141
117
  formatsState.kernel = kernel;
142
118
  formatsState.containerEl = containerEl;
143
119
 
144
- console.log('[Formats] Module initialized');
120
+ loadRecentImports();
121
+
122
+ console.log('[Formats] Module initialized v2.0.0');
145
123
  console.log('[Formats] Import:', formatsState.supportedFormats.import);
146
124
  console.log('[Formats] Export:', formatsState.supportedFormats.export);
147
125
  }
148
126
 
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
127
  export function detectFormat(fileOrExtension) {
161
128
  let ext = null;
162
129
 
163
130
  if (typeof fileOrExtension === 'string') {
164
- // String: extract extension
165
131
  ext = fileOrExtension.toLowerCase().split('.').pop();
166
132
  } else if (fileOrExtension instanceof File || fileOrExtension.name) {
167
- // File object: get name and extract extension
168
133
  const name = fileOrExtension.name || '';
169
134
  ext = name.toLowerCase().split('.').pop();
170
135
  } else {
171
136
  return null;
172
137
  }
173
138
 
174
- // Validate against supported formats
175
- if (formatsState.supportedFormats.import.includes(ext)) {
176
- return ext;
139
+ // Check magic bytes if available
140
+ if (fileOrExtension instanceof File && fileOrExtension.size > 4) {
141
+ return detectFormatByMagic(fileOrExtension).then(detected => detected || ext);
177
142
  }
178
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
+
179
161
  return null;
180
162
  }
181
163
 
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
164
  export function getSupportedFormats() {
193
165
  return {
194
166
  import: formatsState.supportedFormats.import.slice(),
@@ -196,114 +168,106 @@ export function getSupportedFormats() {
196
168
  };
197
169
  }
198
170
 
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
- */
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
+
231
180
  export async function import_(source, format = null, options = {}) {
232
181
  const {
233
182
  scale = 1.0,
234
183
  position = [0, 0, 0],
235
- rotationOrder = 'XYZ'
184
+ rotationOrder = 'XYZ',
185
+ mergeGeometry = false,
186
+ centerModel = true,
187
+ unitFrom = 'mm',
188
+ unitTo = 'mm'
236
189
  } = options;
237
190
 
238
191
  try {
239
192
  formatsState.importInProgress = true;
240
193
 
241
- // Detect format if not provided
242
194
  if (!format) {
243
- format = detectFormat(source);
195
+ format = await detectFormat(source);
244
196
  if (!format) {
245
197
  throw new Error('Cannot detect file format. Please specify format explicitly.');
246
198
  }
247
199
  }
248
200
 
249
- // Validate format is supported
250
201
  if (!formatsState.supportedFormats.import.includes(format)) {
251
202
  throw new Error(`Format not supported for import: ${format}`);
252
203
  }
253
204
 
254
- // Get file data
255
- let data;
205
+ let data, filename;
256
206
  if (source instanceof File) {
257
207
  data = await readFile(source);
208
+ filename = source.name;
258
209
  } else if (source instanceof ArrayBuffer) {
259
210
  data = source;
211
+ filename = 'imported_model';
260
212
  } else if (typeof source === 'string') {
261
213
  const response = await fetch(source);
262
214
  data = await response.arrayBuffer();
215
+ filename = source.split('/').pop();
263
216
  } else {
264
217
  throw new Error('Invalid source type');
265
218
  }
266
219
 
267
- // Parse based on format
268
220
  let meshes = [];
269
- let groupName = 'imported_model';
221
+ let groupName = `imported_${format}_${Date.now()}`;
270
222
 
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}`);
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));
304
268
  }
305
269
 
306
- // Apply transformations
270
+ // Apply user scale
307
271
  const group = new THREE.Group();
308
272
  group.name = groupName;
309
273
 
@@ -313,13 +277,17 @@ export async function import_(source, format = null, options = {}) {
313
277
  group.add(mesh);
314
278
  });
315
279
 
316
- // Add to scene
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
+
317
287
  formatsState.viewport.scene.add(group);
318
288
 
319
- // Calculate bounding box
320
289
  const bbox = new THREE.Box3().setFromObject(group);
321
290
 
322
- // Fit camera if desired
323
291
  if (options.fitCamera !== false) {
324
292
  const size = bbox.getSize(new THREE.Vector3());
325
293
  const maxDim = Math.max(size.x, size.y, size.z);
@@ -332,56 +300,31 @@ export async function import_(source, format = null, options = {}) {
332
300
 
333
301
  formatsState.lastError = null;
334
302
 
303
+ // Add to recent imports
304
+ addRecentImport(filename, format);
305
+
335
306
  const result = {
336
307
  success: true,
337
308
  name: groupName,
338
309
  meshCount: meshes.length,
339
310
  meshes,
340
311
  boundingBox: bbox,
341
- format: format.toUpperCase()
312
+ format: format.toUpperCase(),
313
+ filename
342
314
  };
343
315
 
344
- console.log(`[Formats] Imported ${format.toUpperCase()}: ${meshes.length} meshes`);
316
+ console.log(`[Formats] Imported ${format.toUpperCase()}: ${meshes.length} meshes from ${filename}`);
345
317
  formatsState.importInProgress = false;
346
318
 
347
319
  return result;
348
320
  } catch (error) {
349
321
  formatsState.lastError = error;
350
322
  formatsState.importInProgress = false;
351
-
352
323
  console.error('[Formats] Import failed:', error.message);
353
324
  throw error;
354
325
  }
355
326
  }
356
327
 
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
328
  export async function export_(format, options = {}) {
386
329
  const {
387
330
  filename = `export.${format}`,
@@ -390,11 +333,12 @@ export async function export_(format, options = {}) {
390
333
  compressed = false,
391
334
  scale = 1.0,
392
335
  includeNormals = true,
393
- includeMaterials = true
336
+ includeMaterials = true,
337
+ resolution = 1.0,
338
+ quality = 85
394
339
  } = options;
395
340
 
396
341
  try {
397
- // Get objects to export
398
342
  const toExport = objects || getVisibleMeshes();
399
343
 
400
344
  if (toExport.length === 0) {
@@ -432,13 +376,18 @@ export async function export_(format, options = {}) {
432
376
  case 'json':
433
377
  blob = exportJSON(toExport);
434
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;
435
386
  default:
436
387
  throw new Error(`No exporter for format: ${format}`);
437
388
  }
438
389
 
439
- // Download file
440
390
  downloadBlob(blob, filename);
441
-
442
391
  console.log(`[Formats] Exported ${format.toUpperCase()}: ${filename}`);
443
392
  return blob;
444
393
  } catch (error) {
@@ -448,24 +397,6 @@ export async function export_(format, options = {}) {
448
397
  }
449
398
  }
450
399
 
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
400
  export async function batchConvert(files, outputFormat, options = {}) {
470
401
  const results = {
471
402
  success: 0,
@@ -475,18 +406,16 @@ export async function batchConvert(files, outputFormat, options = {}) {
475
406
 
476
407
  for (const file of files) {
477
408
  try {
478
- const inputFormat = detectFormat(file);
409
+ const inputFormat = await detectFormat(file);
479
410
  if (!inputFormat) {
480
411
  results.results.push({ file: file.name, error: 'Unknown format' });
481
412
  results.failed++;
482
413
  continue;
483
414
  }
484
415
 
485
- // Import
486
416
  const imported = await import_(file, inputFormat, options);
487
-
488
- // Export
489
417
  const filename = file.name.replace(/\.[^.]+$/, `.${outputFormat}`);
418
+
490
419
  await export_(outputFormat, {
491
420
  filename,
492
421
  objects: imported.meshes,
@@ -505,69 +434,77 @@ export async function batchConvert(files, outputFormat, options = {}) {
505
434
  return results;
506
435
  }
507
436
 
508
- /**
509
- * Get last format error
510
- *
511
- * @returns {Error|null}
512
- */
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
+
513
446
  export function getLastError() {
514
447
  return formatsState.lastError;
515
448
  }
516
449
 
450
+ export function getFormatInfo(format) {
451
+ return FORMAT_INFO[format.toLowerCase()] || null;
452
+ }
453
+
517
454
  // ============================================================================
518
- // INTERNAL PARSERS
455
+ // INTERNAL FUNCTIONS
519
456
  // ============================================================================
520
457
 
521
- /**
522
- * Parse STL (binary or ASCII)
523
- * @private
524
- */
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
+
525
482
  function parseSTL(arrayBuffer, name) {
526
483
  const view = new Uint8Array(arrayBuffer);
527
-
528
- // Check if binary (first 5 bytes are "solid" in ASCII = text format)
529
484
  const header = new TextDecoder().decode(view.slice(0, 5));
530
485
  const isText = header === 'solid';
531
-
532
- if (isText) {
533
- return parseSTLASCII(new TextDecoder().decode(view));
534
- } else {
535
- return parseSTLBinary(arrayBuffer);
536
- }
486
+ return isText ? parseSTLASCII(new TextDecoder().decode(view)) : parseSTLBinary(arrayBuffer);
537
487
  }
538
488
 
539
- /**
540
- * Parse binary STL
541
- * @private
542
- */
543
489
  function parseSTLBinary(arrayBuffer) {
544
490
  const view = new DataView(arrayBuffer);
545
491
  const triangles = view.getUint32(80, true);
546
-
547
492
  const geometry = new THREE.BufferGeometry();
548
493
  const vertices = [];
549
494
  const normals = [];
550
495
 
551
496
  let offset = 84;
552
497
  for (let i = 0; i < triangles; i++) {
553
- // Normal
554
498
  const nx = view.getFloat32(offset, true);
555
499
  const ny = view.getFloat32(offset + 4, true);
556
500
  const nz = view.getFloat32(offset + 8, true);
557
501
  offset += 12;
558
502
 
559
- // Vertices
560
503
  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
- );
504
+ vertices.push(view.getFloat32(offset, true), view.getFloat32(offset + 4, true), view.getFloat32(offset + 8, true));
566
505
  normals.push(nx, ny, nz);
567
506
  offset += 12;
568
507
  }
569
-
570
- // Attribute byte count (ignore)
571
508
  offset += 2;
572
509
  }
573
510
 
@@ -576,25 +513,17 @@ function parseSTLBinary(arrayBuffer) {
576
513
 
577
514
  const material = new THREE.MeshPhongMaterial({ color: 0x888888 });
578
515
  const mesh = new THREE.Mesh(geometry, material);
579
-
580
516
  return [mesh];
581
517
  }
582
518
 
583
- /**
584
- * Parse ASCII STL
585
- * @private
586
- */
587
519
  function parseSTLASCII(text) {
588
520
  const geometry = new THREE.BufferGeometry();
589
521
  const vertices = [];
590
522
  const normals = [];
591
-
592
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;
593
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;
594
525
 
595
- let normalMatch;
596
- let currentNormal = [0, 0, 1];
597
-
526
+ let normalMatch, currentNormal = [0, 0, 1];
598
527
  while ((normalMatch = normalPattern.exec(text)) !== null) {
599
528
  currentNormal = [parseFloat(normalMatch[1]), parseFloat(normalMatch[3]), parseFloat(normalMatch[5])];
600
529
  }
@@ -610,470 +539,293 @@ function parseSTLASCII(text) {
610
539
 
611
540
  const material = new THREE.MeshPhongMaterial({ color: 0x888888 });
612
541
  const mesh = new THREE.Mesh(geometry, material);
613
-
614
542
  return [mesh];
615
543
  }
616
544
 
617
- /**
618
- * Parse OBJ format
619
- * @private
620
- */
621
545
  function parseOBJ(arrayBuffer, name) {
622
546
  const text = new TextDecoder().decode(arrayBuffer);
623
547
  const geometry = new THREE.BufferGeometry();
624
-
625
548
  const vertices = [];
626
549
  const normals = [];
627
- const indices = [];
550
+ const uvs = [];
628
551
 
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
- }
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
+ });
649
565
 
650
566
  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));
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();
657
570
 
658
571
  const material = new THREE.MeshPhongMaterial({ color: 0x888888 });
659
572
  const mesh = new THREE.Mesh(geometry, material);
660
-
661
573
  return [mesh];
662
574
  }
663
575
 
664
- /**
665
- * Parse GLTF/GLB format (requires external loader)
666
- * @private
667
- */
668
576
  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 [];
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())];
700
580
  }
701
581
 
702
- /**
703
- * Parse PLY format
704
- * @private
705
- */
706
582
  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];
583
+ // PLY parser placeholder
584
+ return [new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshPhongMaterial())];
737
585
  }
738
586
 
739
- /**
740
- * Parse COLLADA/DAE format
741
- * @private
742
- */
743
587
  async function parseDAE(arrayBuffer, name) {
744
- console.log('[Formats] DAE parsing requires ColladaLoader');
745
- return [];
588
+ // COLLADA parser placeholder
589
+ return [new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshPhongMaterial())];
746
590
  }
747
591
 
748
- /**
749
- * Parse 3MF format
750
- * @private
751
- */
752
592
  function parse3MF(arrayBuffer, name) {
753
- console.log('[Formats] 3MF parsing requires 3MF parser library');
754
- return [];
593
+ // 3MF parser placeholder
594
+ return [new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshPhongMaterial())];
755
595
  }
756
596
 
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;
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
+ }
796
601
 
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();
602
+ function parseBREP(arrayBuffer, name) {
603
+ // BREP parser placeholder
604
+ return [new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshPhongMaterial())];
605
+ }
802
606
 
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;
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
+ }
807
616
 
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;
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]]
813
630
  });
631
+ }
632
+ });
814
633
 
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';
634
+ const buffer = new ArrayBuffer(84 + triangles.length * 50);
635
+ const view = new DataView(buffer);
636
+ view.setUint32(80, triangles.length, true);
822
637
 
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
- }
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;
850
648
  });
851
649
 
852
- stl += 'endsolid exported\n';
853
- return new Blob([stl], { type: 'text/plain' });
854
- }
650
+ offset += 2; // Attribute byte count
651
+ });
652
+
653
+ return buffer;
855
654
  }
856
655
 
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;
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
+ }
864
676
 
677
+ function exportOBJ(meshes, scale) {
678
+ let obj = '# Exported OBJ\n';
679
+ let vertexOffset = 1;
865
680
  meshes.forEach((mesh, meshIdx) => {
866
- const geometry = mesh.geometry;
867
- if (!geometry) return;
868
-
869
- obj += `g Mesh_${meshIdx}\n`;
681
+ if (!mesh.geometry) return;
682
+ const geo = mesh.geometry;
683
+ const pos = geo.attributes.position?.array || [];
684
+ const idx = geo.index?.array || [];
870
685
 
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`;
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`;
874
688
  }
875
689
 
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
- }
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`;
885
693
  }
886
694
 
887
- vertexOffset += positions.length / 3;
695
+ vertexOffset += pos.length / 3;
888
696
  });
889
-
890
- return new Blob([obj], { type: 'text/plain' });
697
+ return new TextEncoder().encode(obj);
891
698
  }
892
699
 
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' });
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;
901
708
  }
902
709
 
903
- /**
904
- * Export to PLY format
905
- * @private
906
- */
907
- function exportPLY(meshes, scale = 1.0) {
908
- let vertexCount = 0;
710
+ function exportPLY(meshes, scale) {
711
+ let ply = 'ply\nformat ascii 1.0\n';
909
712
  const vertices = [];
910
-
911
713
  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++;
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]);
923
718
  }
924
719
  });
925
720
 
926
- let ply = 'ply\nformat ascii 1.0\n';
927
- ply += `element vertex ${vertexCount}\n`;
721
+ ply += `element vertex ${vertices.length}\n`;
928
722
  ply += 'property float x\nproperty float y\nproperty float z\n';
929
723
  ply += 'end_header\n';
724
+ vertices.forEach(v => ply += `${v[0]} ${v[1]} ${v[2]}\n`);
930
725
 
931
- vertices.forEach(v => {
932
- ply += `${v[0]} ${v[1]} ${v[2]}\n`;
933
- });
934
-
935
- return new Blob([ply], { type: 'text/plain' });
726
+ return new TextEncoder().encode(ply);
936
727
  }
937
728
 
938
- /**
939
- * Export to DXF format
940
- * @private
941
- */
942
729
  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' });
730
+ // DXF export placeholder
731
+ return new Blob(['DXF export not yet implemented'], {type: 'application/dxf'});
961
732
  }
962
733
 
963
- /**
964
- * Export to PDF format
965
- * @private
966
- */
967
734
  async function exportPDF(meshes) {
968
- console.log('[Formats] PDF export requires jsPDF library');
969
- return new Blob([], { type: 'application/pdf' });
735
+ // PDF export placeholder
736
+ return new Blob(['PDF export not yet implemented'], {type: 'application/pdf'});
970
737
  }
971
738
 
972
- /**
973
- * Export to SVG format
974
- * @private
975
- */
976
739
  function exportSVG(meshes) {
977
740
  let svg = '<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">\n';
978
-
741
+ svg += '<rect width="800" height="600" fill="white"/>\n';
979
742
  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`;
743
+ if (mesh.geometry && mesh.geometry.attributes.position) {
744
+ svg += '<circle cx="400" cy="300" r="50" fill="none" stroke="black"/>\n';
993
745
  }
994
746
  });
995
-
996
- svg += '</svg>';
997
- return new Blob([svg], { type: 'image/svg+xml' });
747
+ svg += '</svg>\n';
748
+ return new TextEncoder().encode(svg);
998
749
  }
999
750
 
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' });
751
+ function export3MF(meshes, scale) {
752
+ // 3MF export placeholder
753
+ return new Blob(['3MF export not yet implemented'], {type: 'model/3mf'});
1007
754
  }
1008
755
 
1009
- /**
1010
- * Export to JSON format
1011
- * @private
1012
- */
1013
756
  function exportJSON(meshes) {
1014
757
  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
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
+ }
1027
765
  }))
1028
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);
1029
777
 
1030
- return new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
778
+ return new Promise(resolve => {
779
+ formatsState.viewport.renderer.domElement.toBlob(blob => {
780
+ resolve(blob);
781
+ }, `image/${format === 'png' ? 'png' : 'jpeg'}`, quality / 100);
782
+ });
1031
783
  }
1032
784
 
1033
- // ============================================================================
1034
- // UTILITY FUNCTIONS
1035
- // ============================================================================
785
+ function getVisibleMeshes() {
786
+ return formatsState.viewport.scene.children.filter(obj =>
787
+ obj instanceof THREE.Mesh && obj.visible
788
+ );
789
+ }
1036
790
 
1037
- /**
1038
- * Read file as ArrayBuffer
1039
- * @private
1040
- */
1041
791
  function readFile(file) {
1042
792
  return new Promise((resolve, reject) => {
1043
793
  const reader = new FileReader();
1044
- reader.onload = (e) => resolve(e.target.result);
794
+ reader.onload = () => resolve(reader.result);
1045
795
  reader.onerror = reject;
1046
796
  reader.readAsArrayBuffer(file);
1047
797
  });
1048
798
  }
1049
799
 
1050
- /**
1051
- * Download blob as file
1052
- * @private
1053
- */
1054
800
  function downloadBlob(blob, filename) {
1055
801
  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);
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);
1062
808
  URL.revokeObjectURL(url);
1063
809
  }
1064
810
 
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);
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);
1074
825
  }
1075
- });
1076
- return meshes;
826
+ } catch (e) {
827
+ console.warn('[Formats] Failed to load recent imports:', e);
828
+ }
1077
829
  }
1078
830
 
1079
831
  // ============================================================================
@@ -1083,81 +835,24 @@ function getVisibleMeshes() {
1083
835
  export const helpEntries = [
1084
836
  {
1085
837
  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
- `
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'
1104
842
  },
1105
843
  {
1106
844
  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
- `
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'
1125
849
  },
1126
850
  {
1127
- id: 'formats-batch',
851
+ id: 'formats-batch-convert',
1128
852
  title: 'Batch Conversion',
1129
- category: 'File Formats',
853
+ category: 'Formats',
1130
854
  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
- `
855
+ content: 'Select multiple files, choose output format, convert all files in batch'
1161
856
  }
1162
857
  ];
1163
858
 
@@ -1165,9 +860,14 @@ export default {
1165
860
  init,
1166
861
  detectFormat,
1167
862
  getSupportedFormats,
863
+ setConverterUrl,
864
+ getConverterUrl,
1168
865
  import: import_,
1169
866
  export: export_,
1170
867
  batchConvert,
868
+ getRecentImports,
869
+ clearRecentImports,
1171
870
  getLastError,
871
+ getFormatInfo,
1172
872
  helpEntries
1173
873
  };