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.
- 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/MODULE_API_REFERENCE.md +712 -0
- package/MODULE_INVENTORY.txt +264 -0
- package/app/index.html +1342 -5031
- package/app/js/app.js +1312 -514
- package/app/js/modules/animation-module.js +497 -3
- package/app/js/modules/cam-module.js +507 -2
- package/app/js/modules/collaboration-module.js +513 -0
- package/app/js/modules/constraint-module.js +1266 -0
- package/app/js/modules/data-module.js +544 -1146
- package/app/js/modules/formats-module.js +438 -738
- package/app/js/modules/inspection-module.js +393 -0
- package/app/js/modules/mesh-module-enhanced.js +880 -0
- package/app/js/modules/plugin-module.js +597 -0
- package/app/js/modules/rendering-module.js +460 -0
- package/app/js/modules/scripting-module.js +593 -475
- package/app/js/modules/sketch-module.js +998 -2
- package/app/js/modules/surface-module.js +312 -0
- package/app/js/modules/version-module.js +420 -0
- package/cycleCAD-Architecture-v2.pptx +0 -0
- package/package.json +1 -1
- package/~$cycleCAD-Architecture-v2.pptx +0 -0
|
@@ -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
|
-
*
|
|
5
|
+
* 15+ CAD, geometry, and data exchange formats with full metadata handling.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
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
|
-
* -
|
|
22
|
-
* -
|
|
23
|
-
* -
|
|
24
|
-
* -
|
|
25
|
-
* -
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
28
|
-
* -
|
|
29
|
-
* -
|
|
30
|
-
* -
|
|
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
|
|
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
|
-
//
|
|
79
|
+
// FORMAT METADATA
|
|
108
80
|
// ============================================================================
|
|
109
81
|
|
|
110
82
|
const FORMAT_INFO = {
|
|
111
|
-
'step': { name: 'STEP',
|
|
112
|
-
'stp': { name: 'STEP',
|
|
113
|
-
'iges': { name: 'IGES',
|
|
114
|
-
'igs': { name: 'IGES',
|
|
115
|
-
'stl': { name: 'STL',
|
|
116
|
-
'obj': { name: 'OBJ',
|
|
117
|
-
'gltf': { name: 'glTF',
|
|
118
|
-
'glb': { name: 'GLB',
|
|
119
|
-
'dxf': { name: 'DXF',
|
|
120
|
-
'dae': { name: 'COLLADA',
|
|
121
|
-
'3mf': { name: '3MF',
|
|
122
|
-
'ply': { name: 'PLY',
|
|
123
|
-
'pdf': { name: 'PDF',
|
|
124
|
-
'svg': { name: 'SVG',
|
|
125
|
-
'json': { name: 'JSON',
|
|
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
|
-
|
|
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
|
-
//
|
|
175
|
-
if (
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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 =
|
|
221
|
+
let groupName = `imported_${format}_${Date.now()}`;
|
|
270
222
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
455
|
+
// INTERNAL FUNCTIONS
|
|
519
456
|
// ============================================================================
|
|
520
457
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
|
550
|
+
const uvs = [];
|
|
628
551
|
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
653
|
-
|
|
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:
|
|
670
|
-
console.
|
|
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
|
-
|
|
708
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
|
|
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
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
650
|
+
offset += 2; // Attribute byte count
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
return buffer;
|
|
855
654
|
}
|
|
856
655
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
872
|
-
|
|
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
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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 +=
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
969
|
-
return new Blob([], {
|
|
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
|
-
|
|
981
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
|
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
|
-
|
|
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 = (
|
|
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
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
document.body.appendChild(
|
|
1060
|
-
|
|
1061
|
-
document.body.removeChild(
|
|
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
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
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
|
|
1087
|
-
category: '
|
|
1088
|
-
description: '
|
|
1089
|
-
|
|
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
|
|
1108
|
-
category: '
|
|
1109
|
-
description: '
|
|
1110
|
-
|
|
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: '
|
|
853
|
+
category: 'Formats',
|
|
1130
854
|
description: 'Convert multiple files at once',
|
|
1131
|
-
|
|
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
|
};
|