cyclecad 2.0.1 → 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.
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/app/index.html +106 -2
- package/app/js/brep-kernel.js +1353 -455
- package/app/js/help-module.js +1437 -0
- package/app/js/kernel.js +364 -40
- package/app/js/modules/animation-module.js +967 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1067 -0
- package/app/js/modules/collaboration-module.js +1102 -0
- package/app/js/modules/data-module.js +1656 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +1173 -0
- package/app/js/modules/inspection-module.js +937 -0
- package/app/js/modules/mesh-module.js +968 -0
- package/app/js/modules/operations-module.js +40 -7
- package/app/js/modules/plugin-module.js +957 -0
- package/app/js/modules/rendering-module.js +1306 -0
- package/app/js/modules/scripting-module.js +955 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +1032 -90
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +728 -0
- package/app/js/modules/version-module.js +1410 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/docs/ARCHITECTURE.html +838 -1408
- package/docs/DEVELOPER-GUIDE.md +1504 -0
- package/docs/TUTORIAL.md +740 -0
- package/package.json +1 -1
- package/.github/scripts/cad-diff.js +0 -590
- 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
|
+
};
|