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