cyclecad 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CNAME +1 -0
- package/app/docs/api-reference.html +1436 -0
- package/app/docs/examples.html +803 -0
- package/app/docs/getting-started.html +1620 -0
- package/app/duo-project-browser.html +1321 -0
- package/app/duo-rebuild-guide.html +861 -0
- package/app/index.html +1635 -0
- package/app/js/ai-chat.js +992 -0
- package/app/js/app.js +724 -0
- package/app/js/export.js +658 -0
- package/app/js/inventor-parser.js +1138 -0
- package/app/js/operations.js +689 -0
- package/app/js/params.js +523 -0
- package/app/js/reverse-engineer.js +1275 -0
- package/app/js/shortcuts.js +350 -0
- package/app/js/sketch.js +899 -0
- package/app/js/tree.js +479 -0
- package/app/js/viewport.js +643 -0
- package/app/samples/Leistenbuerstenblech.ipt +0 -0
- package/app/samples/Rahmen_Seite.iam +0 -0
- package/app/samples/TraegerHoehe1.ipt +0 -0
- package/index.html +1226 -0
- package/package.json +33 -0
package/app/js/export.js
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* export.js - cycleCAD Export Module
|
|
3
|
+
* Handles exporting 3D models in various formats (STL, OBJ, glTF, STEP, JSON)
|
|
4
|
+
* and importing cycleCAD native JSON format
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Download a file to the user's computer
|
|
11
|
+
* @param {string|ArrayBuffer} content - File content
|
|
12
|
+
* @param {string} filename - Filename for download
|
|
13
|
+
* @param {string} mimeType - MIME type (e.g., 'text/plain', 'application/octet-stream')
|
|
14
|
+
*/
|
|
15
|
+
export function downloadFile(content, filename, mimeType) {
|
|
16
|
+
let blob;
|
|
17
|
+
if (content instanceof ArrayBuffer) {
|
|
18
|
+
blob = new Blob([content], { type: mimeType });
|
|
19
|
+
} else {
|
|
20
|
+
blob = new Blob([content], { type: mimeType });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const url = URL.createObjectURL(blob);
|
|
24
|
+
const link = document.createElement('a');
|
|
25
|
+
link.href = url;
|
|
26
|
+
link.download = filename;
|
|
27
|
+
document.body.appendChild(link);
|
|
28
|
+
link.click();
|
|
29
|
+
document.body.removeChild(link);
|
|
30
|
+
URL.revokeObjectURL(url);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract triangles from a Three.js BufferGeometry
|
|
35
|
+
* Handles both indexed and non-indexed geometries
|
|
36
|
+
* @param {THREE.BufferGeometry} geometry - The geometry to extract from
|
|
37
|
+
* @returns {Array<{vertices: number[][], normal: number[]}>} Array of triangles
|
|
38
|
+
*/
|
|
39
|
+
function extractTrianglesFromGeometry(geometry) {
|
|
40
|
+
const triangles = [];
|
|
41
|
+
const positions = geometry.getAttribute('position');
|
|
42
|
+
const normals = geometry.getAttribute('normal');
|
|
43
|
+
const indices = geometry.getIndex();
|
|
44
|
+
|
|
45
|
+
if (!positions) return triangles;
|
|
46
|
+
|
|
47
|
+
const posArray = positions.array;
|
|
48
|
+
const normArray = normals ? normals.array : null;
|
|
49
|
+
const indexArray = indices ? indices.array : null;
|
|
50
|
+
|
|
51
|
+
let triangleCount;
|
|
52
|
+
if (indexArray) {
|
|
53
|
+
triangleCount = indexArray.length / 3;
|
|
54
|
+
} else {
|
|
55
|
+
triangleCount = posArray.length / 9; // 3 vertices * 3 coords per triangle
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < triangleCount; i++) {
|
|
59
|
+
let idx0, idx1, idx2;
|
|
60
|
+
|
|
61
|
+
if (indexArray) {
|
|
62
|
+
idx0 = indexArray[i * 3] * 3;
|
|
63
|
+
idx1 = indexArray[i * 3 + 1] * 3;
|
|
64
|
+
idx2 = indexArray[i * 3 + 2] * 3;
|
|
65
|
+
} else {
|
|
66
|
+
idx0 = i * 9;
|
|
67
|
+
idx1 = i * 9 + 3;
|
|
68
|
+
idx2 = i * 9 + 6;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const v0 = [posArray[idx0], posArray[idx0 + 1], posArray[idx0 + 2]];
|
|
72
|
+
const v1 = [posArray[idx1], posArray[idx1 + 1], posArray[idx1 + 2]];
|
|
73
|
+
const v2 = [posArray[idx2], posArray[idx2 + 1], posArray[idx2 + 2]];
|
|
74
|
+
|
|
75
|
+
let normal = [0, 0, 1];
|
|
76
|
+
if (normArray) {
|
|
77
|
+
const nIdx = (indexArray ? indexArray[i * 3] : i * 3) * 3;
|
|
78
|
+
normal = [normArray[nIdx], normArray[nIdx + 1], normArray[nIdx + 2]];
|
|
79
|
+
} else {
|
|
80
|
+
// Calculate normal from vertices if not provided
|
|
81
|
+
const edge1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
|
|
82
|
+
const edge2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
|
|
83
|
+
normal = crossProduct(edge1, edge2);
|
|
84
|
+
normal = normalizeVector(normal);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
triangles.push({
|
|
88
|
+
vertices: [v0, v1, v2],
|
|
89
|
+
normal: normal
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return triangles;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Calculate cross product of two 3D vectors
|
|
98
|
+
* @param {number[]} a - Vector A
|
|
99
|
+
* @param {number[]} b - Vector B
|
|
100
|
+
* @returns {number[]} Cross product
|
|
101
|
+
*/
|
|
102
|
+
function crossProduct(a, b) {
|
|
103
|
+
return [
|
|
104
|
+
a[1] * b[2] - a[2] * b[1],
|
|
105
|
+
a[2] * b[0] - a[0] * b[2],
|
|
106
|
+
a[0] * b[1] - a[1] * b[0]
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Normalize a 3D vector
|
|
112
|
+
* @param {number[]} v - Vector to normalize
|
|
113
|
+
* @returns {number[]} Normalized vector
|
|
114
|
+
*/
|
|
115
|
+
function normalizeVector(v) {
|
|
116
|
+
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
117
|
+
if (length === 0) return [0, 0, 1];
|
|
118
|
+
return [v[0] / length, v[1] / length, v[2] / length];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Transform a 3D point by a 4x4 matrix
|
|
123
|
+
* @param {number[]} point - Point [x, y, z]
|
|
124
|
+
* @param {THREE.Matrix4} matrix - Transform matrix
|
|
125
|
+
* @returns {number[]} Transformed point
|
|
126
|
+
*/
|
|
127
|
+
function transformPoint(point, matrix) {
|
|
128
|
+
const v = new THREE.Vector3(point[0], point[1], point[2]);
|
|
129
|
+
v.applyMatrix4(matrix);
|
|
130
|
+
return [v.x, v.y, v.z];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Export features as ASCII STL format
|
|
135
|
+
* @param {Array} features - Array of feature objects with mesh property
|
|
136
|
+
* @param {string} filename - Output filename
|
|
137
|
+
*/
|
|
138
|
+
export function exportSTL(features, filename = 'model.stl') {
|
|
139
|
+
let stlContent = 'solid cycleCAD_Model\n';
|
|
140
|
+
|
|
141
|
+
features.forEach((feature, fIdx) => {
|
|
142
|
+
if (!feature.mesh || !feature.mesh.geometry) return;
|
|
143
|
+
|
|
144
|
+
const geometry = feature.mesh.geometry.clone();
|
|
145
|
+
geometry.computeVertexNormals();
|
|
146
|
+
|
|
147
|
+
const triangles = extractTrianglesFromGeometry(geometry);
|
|
148
|
+
const worldMatrix = feature.mesh.matrixWorld;
|
|
149
|
+
|
|
150
|
+
triangles.forEach(triangle => {
|
|
151
|
+
const v0 = transformPoint(triangle.vertices[0], worldMatrix);
|
|
152
|
+
const v1 = transformPoint(triangle.vertices[1], worldMatrix);
|
|
153
|
+
const v2 = transformPoint(triangle.vertices[2], worldMatrix);
|
|
154
|
+
|
|
155
|
+
// Recalculate normal from world-space vertices
|
|
156
|
+
const edge1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
|
|
157
|
+
const edge2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
|
|
158
|
+
const normal = normalizeVector(crossProduct(edge1, edge2));
|
|
159
|
+
|
|
160
|
+
stlContent += ` facet normal ${normal[0].toFixed(8)} ${normal[1].toFixed(8)} ${normal[2].toFixed(8)}\n`;
|
|
161
|
+
stlContent += ` outer loop\n`;
|
|
162
|
+
stlContent += ` vertex ${v0[0].toFixed(8)} ${v0[1].toFixed(8)} ${v0[2].toFixed(8)}\n`;
|
|
163
|
+
stlContent += ` vertex ${v1[0].toFixed(8)} ${v1[1].toFixed(8)} ${v1[2].toFixed(8)}\n`;
|
|
164
|
+
stlContent += ` vertex ${v2[0].toFixed(8)} ${v2[1].toFixed(8)} ${v2[2].toFixed(8)}\n`;
|
|
165
|
+
stlContent += ` endloop\n`;
|
|
166
|
+
stlContent += ` endfacet\n`;
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
stlContent += 'endsolid cycleCAD_Model\n';
|
|
171
|
+
downloadFile(stlContent, filename, 'text/plain');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Export features as Binary STL format (more compact)
|
|
176
|
+
* @param {Array} features - Array of feature objects with mesh property
|
|
177
|
+
* @param {string} filename - Output filename
|
|
178
|
+
*/
|
|
179
|
+
export function exportSTLBinary(features, filename = 'model.stl') {
|
|
180
|
+
const triangles = [];
|
|
181
|
+
|
|
182
|
+
features.forEach((feature, fIdx) => {
|
|
183
|
+
if (!feature.mesh || !feature.mesh.geometry) return;
|
|
184
|
+
|
|
185
|
+
const geometry = feature.mesh.geometry.clone();
|
|
186
|
+
geometry.computeVertexNormals();
|
|
187
|
+
|
|
188
|
+
const tris = extractTrianglesFromGeometry(geometry);
|
|
189
|
+
const worldMatrix = feature.mesh.matrixWorld;
|
|
190
|
+
|
|
191
|
+
tris.forEach(triangle => {
|
|
192
|
+
const v0 = transformPoint(triangle.vertices[0], worldMatrix);
|
|
193
|
+
const v1 = transformPoint(triangle.vertices[1], worldMatrix);
|
|
194
|
+
const v2 = transformPoint(triangle.vertices[2], worldMatrix);
|
|
195
|
+
|
|
196
|
+
const edge1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
|
|
197
|
+
const edge2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
|
|
198
|
+
const normal = normalizeVector(crossProduct(edge1, edge2));
|
|
199
|
+
|
|
200
|
+
triangles.push({
|
|
201
|
+
normal: normal,
|
|
202
|
+
vertices: [v0, v1, v2]
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Create binary buffer
|
|
208
|
+
// 80-byte header + 4-byte triangle count + (50 bytes per triangle)
|
|
209
|
+
const buffer = new ArrayBuffer(80 + 4 + triangles.length * 50);
|
|
210
|
+
const view = new DataView(buffer);
|
|
211
|
+
|
|
212
|
+
// Header (80 bytes of zeros, can be used for description)
|
|
213
|
+
const headerText = 'cycleCAD STL Binary Export'.padEnd(80, '\0');
|
|
214
|
+
for (let i = 0; i < 80; i++) {
|
|
215
|
+
view.setUint8(i, headerText.charCodeAt(i));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Triangle count (4 bytes, little-endian)
|
|
219
|
+
view.setUint32(80, triangles.length, true);
|
|
220
|
+
|
|
221
|
+
// Triangle data (50 bytes each)
|
|
222
|
+
let offset = 84;
|
|
223
|
+
triangles.forEach(tri => {
|
|
224
|
+
// Normal (3 × float32)
|
|
225
|
+
view.setFloat32(offset, tri.normal[0], true);
|
|
226
|
+
view.setFloat32(offset + 4, tri.normal[1], true);
|
|
227
|
+
view.setFloat32(offset + 8, tri.normal[2], true);
|
|
228
|
+
offset += 12;
|
|
229
|
+
|
|
230
|
+
// Vertices (3 vertices × 3 coords × float32)
|
|
231
|
+
tri.vertices.forEach(vertex => {
|
|
232
|
+
view.setFloat32(offset, vertex[0], true);
|
|
233
|
+
view.setFloat32(offset + 4, vertex[1], true);
|
|
234
|
+
view.setFloat32(offset + 8, vertex[2], true);
|
|
235
|
+
offset += 12;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Attribute byte count (2 bytes, usually 0)
|
|
239
|
+
view.setUint16(offset, 0, true);
|
|
240
|
+
offset += 2;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
downloadFile(buffer, filename, 'application/octet-stream');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Export features as Wavefront OBJ format
|
|
248
|
+
* @param {Array} features - Array of feature objects with mesh property
|
|
249
|
+
* @param {string} filename - Output filename
|
|
250
|
+
*/
|
|
251
|
+
export function exportOBJ(features, filename = 'model.obj') {
|
|
252
|
+
let objContent = '# cycleCAD OBJ Export\n';
|
|
253
|
+
objContent += '# https://github.com/vvlars-cmd/cyclecad\n\n';
|
|
254
|
+
|
|
255
|
+
let vertexOffset = 0;
|
|
256
|
+
let normalOffset = 0;
|
|
257
|
+
|
|
258
|
+
features.forEach((feature, fIdx) => {
|
|
259
|
+
if (!feature.mesh || !feature.mesh.geometry) return;
|
|
260
|
+
|
|
261
|
+
const geometry = feature.mesh.geometry.clone();
|
|
262
|
+
geometry.computeVertexNormals();
|
|
263
|
+
|
|
264
|
+
const positions = geometry.getAttribute('position');
|
|
265
|
+
const normals = geometry.getAttribute('normal');
|
|
266
|
+
const indices = geometry.getIndex();
|
|
267
|
+
|
|
268
|
+
if (!positions) return;
|
|
269
|
+
|
|
270
|
+
objContent += `g feature_${fIdx}\n`;
|
|
271
|
+
objContent += `usemtl material_${fIdx}\n`;
|
|
272
|
+
|
|
273
|
+
const posArray = positions.array;
|
|
274
|
+
const normArray = normals ? normals.array : null;
|
|
275
|
+
const worldMatrix = feature.mesh.matrixWorld;
|
|
276
|
+
|
|
277
|
+
// Write vertices
|
|
278
|
+
const vertexCount = posArray.length / 3;
|
|
279
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
280
|
+
const v = transformPoint(
|
|
281
|
+
[posArray[i * 3], posArray[i * 3 + 1], posArray[i * 3 + 2]],
|
|
282
|
+
worldMatrix
|
|
283
|
+
);
|
|
284
|
+
objContent += `v ${v[0].toFixed(8)} ${v[1].toFixed(8)} ${v[2].toFixed(8)}\n`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Write normals
|
|
288
|
+
if (normArray) {
|
|
289
|
+
const normCount = normArray.length / 3;
|
|
290
|
+
for (let i = 0; i < normCount; i++) {
|
|
291
|
+
objContent += `vn ${normArray[i * 3].toFixed(8)} ${normArray[i * 3 + 1].toFixed(8)} ${normArray[i * 3 + 2].toFixed(8)}\n`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Write faces
|
|
296
|
+
const indexArray = indices ? indices.array : null;
|
|
297
|
+
let triangleCount;
|
|
298
|
+
if (indexArray) {
|
|
299
|
+
triangleCount = indexArray.length / 3;
|
|
300
|
+
} else {
|
|
301
|
+
triangleCount = posArray.length / 9;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (let i = 0; i < triangleCount; i++) {
|
|
305
|
+
let idx0, idx1, idx2;
|
|
306
|
+
|
|
307
|
+
if (indexArray) {
|
|
308
|
+
idx0 = indexArray[i * 3] + 1 + vertexOffset; // OBJ uses 1-based indexing
|
|
309
|
+
idx1 = indexArray[i * 3 + 1] + 1 + vertexOffset;
|
|
310
|
+
idx2 = indexArray[i * 3 + 2] + 1 + vertexOffset;
|
|
311
|
+
} else {
|
|
312
|
+
idx0 = i * 3 + 1 + vertexOffset;
|
|
313
|
+
idx1 = i * 3 + 2 + vertexOffset;
|
|
314
|
+
idx2 = i * 3 + 3 + vertexOffset;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (normArray) {
|
|
318
|
+
const nIdx0 = idx0 + normalOffset - 1;
|
|
319
|
+
const nIdx1 = idx1 + normalOffset - 1;
|
|
320
|
+
const nIdx2 = idx2 + normalOffset - 1;
|
|
321
|
+
objContent += `f ${idx0}/${idx0}/${nIdx0} ${idx1}/${idx1}/${nIdx1} ${idx2}/${idx2}/${nIdx2}\n`;
|
|
322
|
+
} else {
|
|
323
|
+
objContent += `f ${idx0} ${idx1} ${idx2}\n`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
vertexOffset += vertexCount;
|
|
328
|
+
if (normArray) normalOffset += normArray.length / 3;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Append MTL reference
|
|
332
|
+
objContent += '\n# Material definitions\n';
|
|
333
|
+
objContent += '# Uncomment below or create separate .mtl file\n';
|
|
334
|
+
features.forEach((feature, fIdx) => {
|
|
335
|
+
objContent += `# newmtl material_${fIdx}\n`;
|
|
336
|
+
objContent += `# Kd 0.8 0.8 0.8\n`;
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
downloadFile(objContent, filename, 'text/plain');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Export features as glTF 2.0 JSON format
|
|
344
|
+
* @param {Array} features - Array of feature objects with mesh property
|
|
345
|
+
* @param {string} filename - Output filename (usually .gltf)
|
|
346
|
+
*/
|
|
347
|
+
export function exportGLTF(features, filename = 'model.gltf') {
|
|
348
|
+
const gltf = {
|
|
349
|
+
asset: {
|
|
350
|
+
version: '2.0',
|
|
351
|
+
generator: 'cycleCAD v0.1'
|
|
352
|
+
},
|
|
353
|
+
scene: 0,
|
|
354
|
+
scenes: [{ nodes: [] }],
|
|
355
|
+
nodes: [],
|
|
356
|
+
meshes: [],
|
|
357
|
+
geometries: [],
|
|
358
|
+
materials: [],
|
|
359
|
+
accessors: [],
|
|
360
|
+
bufferViews: [],
|
|
361
|
+
buffers: []
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
let bufferData = [];
|
|
365
|
+
let bufferViewIndex = 0;
|
|
366
|
+
let accessorIndex = 0;
|
|
367
|
+
let nodeIndex = 0;
|
|
368
|
+
|
|
369
|
+
features.forEach((feature, fIdx) => {
|
|
370
|
+
if (!feature.mesh || !feature.mesh.geometry) return;
|
|
371
|
+
|
|
372
|
+
const geometry = feature.mesh.geometry.clone();
|
|
373
|
+
geometry.computeVertexNormals();
|
|
374
|
+
|
|
375
|
+
const positions = geometry.getAttribute('position');
|
|
376
|
+
const normals = geometry.getAttribute('normal');
|
|
377
|
+
const indices = geometry.getIndex();
|
|
378
|
+
|
|
379
|
+
if (!positions) return;
|
|
380
|
+
|
|
381
|
+
const posArray = positions.array;
|
|
382
|
+
const normArray = normals ? normals.array : null;
|
|
383
|
+
const indexArray = indices ? indices.array : null;
|
|
384
|
+
const worldMatrix = feature.mesh.matrixWorld;
|
|
385
|
+
|
|
386
|
+
// Apply world matrix to positions
|
|
387
|
+
const transformedPos = [];
|
|
388
|
+
const vertexCount = posArray.length / 3;
|
|
389
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
390
|
+
const v = transformPoint(
|
|
391
|
+
[posArray[i * 3], posArray[i * 3 + 1], posArray[i * 3 + 2]],
|
|
392
|
+
worldMatrix
|
|
393
|
+
);
|
|
394
|
+
transformedPos.push(...v);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Create position accessor
|
|
398
|
+
const posAccessor = {
|
|
399
|
+
bufferView: bufferViewIndex,
|
|
400
|
+
componentType: 5126, // FLOAT
|
|
401
|
+
count: vertexCount,
|
|
402
|
+
type: 'VEC3',
|
|
403
|
+
min: [Math.min(...transformedPos.filter((_, i) => i % 3 === 0)),
|
|
404
|
+
Math.min(...transformedPos.filter((_, i) => i % 3 === 1)),
|
|
405
|
+
Math.min(...transformedPos.filter((_, i) => i % 3 === 2))],
|
|
406
|
+
max: [Math.max(...transformedPos.filter((_, i) => i % 3 === 0)),
|
|
407
|
+
Math.max(...transformedPos.filter((_, i) => i % 3 === 1)),
|
|
408
|
+
Math.max(...transformedPos.filter((_, i) => i % 3 === 2))]
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const posAccessorIndex = accessorIndex++;
|
|
412
|
+
gltf.accessors.push(posAccessor);
|
|
413
|
+
|
|
414
|
+
// Add position data to buffer
|
|
415
|
+
const posBuffer = new Float32Array(transformedPos);
|
|
416
|
+
bufferData.push(posBuffer);
|
|
417
|
+
|
|
418
|
+
gltf.bufferViews.push({
|
|
419
|
+
buffer: 0,
|
|
420
|
+
byteOffset: bufferData.reduce((sum, buf) => sum + buf.byteLength, 0) - posBuffer.byteLength,
|
|
421
|
+
byteLength: posBuffer.byteLength,
|
|
422
|
+
target: 34962 // ARRAY_BUFFER
|
|
423
|
+
});
|
|
424
|
+
bufferViewIndex++;
|
|
425
|
+
|
|
426
|
+
// Create normal accessor if available
|
|
427
|
+
let normalAccessorIndex = -1;
|
|
428
|
+
if (normArray) {
|
|
429
|
+
const normAccessor = {
|
|
430
|
+
bufferView: bufferViewIndex,
|
|
431
|
+
componentType: 5126,
|
|
432
|
+
count: vertexCount,
|
|
433
|
+
type: 'VEC3'
|
|
434
|
+
};
|
|
435
|
+
normalAccessorIndex = accessorIndex++;
|
|
436
|
+
gltf.accessors.push(normAccessor);
|
|
437
|
+
|
|
438
|
+
const normBuffer = new Float32Array(normArray);
|
|
439
|
+
bufferData.push(normBuffer);
|
|
440
|
+
|
|
441
|
+
gltf.bufferViews.push({
|
|
442
|
+
buffer: 0,
|
|
443
|
+
byteOffset: bufferData.reduce((sum, buf) => sum + buf.byteLength, 0) - normBuffer.byteLength,
|
|
444
|
+
byteLength: normBuffer.byteLength,
|
|
445
|
+
target: 34962
|
|
446
|
+
});
|
|
447
|
+
bufferViewIndex++;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Create index accessor if available
|
|
451
|
+
let indicesAccessorIndex = -1;
|
|
452
|
+
if (indexArray) {
|
|
453
|
+
const indAccessor = {
|
|
454
|
+
bufferView: bufferViewIndex,
|
|
455
|
+
componentType: 5125, // UNSIGNED_INT
|
|
456
|
+
count: indexArray.length,
|
|
457
|
+
type: 'SCALAR'
|
|
458
|
+
};
|
|
459
|
+
indicesAccessorIndex = accessorIndex++;
|
|
460
|
+
gltf.accessors.push(indAccessor);
|
|
461
|
+
|
|
462
|
+
const indBuffer = new Uint32Array(indexArray);
|
|
463
|
+
bufferData.push(indBuffer);
|
|
464
|
+
|
|
465
|
+
gltf.bufferViews.push({
|
|
466
|
+
buffer: 0,
|
|
467
|
+
byteOffset: bufferData.reduce((sum, buf) => sum + buf.byteLength, 0) - indBuffer.byteLength,
|
|
468
|
+
byteLength: indBuffer.byteLength,
|
|
469
|
+
target: 34963 // ELEMENT_ARRAY_BUFFER
|
|
470
|
+
});
|
|
471
|
+
bufferViewIndex++;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Create primitive
|
|
475
|
+
const primitive = {
|
|
476
|
+
attributes: {
|
|
477
|
+
POSITION: posAccessorIndex
|
|
478
|
+
},
|
|
479
|
+
mode: 4 // TRIANGLES
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
if (normalAccessorIndex >= 0) {
|
|
483
|
+
primitive.attributes.NORMAL = normalAccessorIndex;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (indicesAccessorIndex >= 0) {
|
|
487
|
+
primitive.indices = indicesAccessorIndex;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Create material
|
|
491
|
+
const materialIndex = gltf.materials.length;
|
|
492
|
+
gltf.materials.push({
|
|
493
|
+
pbrMetallicRoughness: {
|
|
494
|
+
baseColorFactor: [0.8, 0.8, 0.8, 1.0],
|
|
495
|
+
metallicFactor: 0.0,
|
|
496
|
+
roughnessFactor: 1.0
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
primitive.material = materialIndex;
|
|
500
|
+
|
|
501
|
+
// Create mesh
|
|
502
|
+
const meshIndex = gltf.meshes.length;
|
|
503
|
+
gltf.meshes.push({
|
|
504
|
+
primitives: [primitive]
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Create node
|
|
508
|
+
const nodeIdx = gltf.nodes.length;
|
|
509
|
+
gltf.nodes.push({
|
|
510
|
+
mesh: meshIndex,
|
|
511
|
+
name: `feature_${fIdx}`
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
gltf.scenes[0].nodes.push(nodeIdx);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Combine all buffer data
|
|
518
|
+
let totalSize = 0;
|
|
519
|
+
bufferData.forEach(buf => totalSize += buf.byteLength);
|
|
520
|
+
|
|
521
|
+
const combinedBuffer = new Uint8Array(totalSize);
|
|
522
|
+
let offset = 0;
|
|
523
|
+
bufferData.forEach(buf => {
|
|
524
|
+
combinedBuffer.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), offset);
|
|
525
|
+
offset += buf.byteLength;
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const base64Buffer = btoa(String.fromCharCode.apply(null, combinedBuffer));
|
|
529
|
+
gltf.buffers = [{
|
|
530
|
+
byteLength: totalSize,
|
|
531
|
+
uri: `data:application/octet-stream;base64,${base64Buffer}`
|
|
532
|
+
}];
|
|
533
|
+
|
|
534
|
+
const gltfContent = JSON.stringify(gltf, null, 2);
|
|
535
|
+
downloadFile(gltfContent, filename, 'application/json');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Export features as STEP format
|
|
540
|
+
* Note: STEP export requires OpenCascade.js to be loaded
|
|
541
|
+
* @param {Array} features - Array of feature objects
|
|
542
|
+
* @param {Object} occt - OpenCascade.js instance (optional)
|
|
543
|
+
* @param {string} filename - Output filename
|
|
544
|
+
*/
|
|
545
|
+
export function exportSTEP(features, occt = null, filename = 'model.step') {
|
|
546
|
+
if (!occt) {
|
|
547
|
+
console.error('STEP export requires OpenCascade.js kernel. Please load it first.');
|
|
548
|
+
alert('STEP export requires OpenCascade.js to be loaded.\n\nAdd this to your HTML:\n<script src="https://cdn.jsdelivr.net/npm/opencascade.js@latest/dist/opencascade.wasm.js"><\/script>');
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
// Create STEP writer
|
|
554
|
+
const step = new occt.STEPControl_Writer();
|
|
555
|
+
|
|
556
|
+
features.forEach((feature, fIdx) => {
|
|
557
|
+
if (!feature.mesh || !feature.mesh.geometry) return;
|
|
558
|
+
|
|
559
|
+
// This is a simplified stub - actual implementation would:
|
|
560
|
+
// 1. Convert Three.js mesh vertices to OCCT vertices
|
|
561
|
+
// 2. Build OCCT edges and wires
|
|
562
|
+
// 3. Create faces from wires
|
|
563
|
+
// 4. Combine into compound shapes
|
|
564
|
+
// 5. Write to STEP
|
|
565
|
+
|
|
566
|
+
// For now, just show a placeholder
|
|
567
|
+
console.warn(`Feature ${fIdx} conversion to OCCT shapes not yet implemented`);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Placeholder: would write step.Write(filename, ...)
|
|
571
|
+
alert('STEP export is currently a stub.\n\nImplementation requires:\n- OpenCascade.js loaded\n- Three.js → OCCT shape conversion\n- STEP writer API calls\n\nFor now, use STL/OBJ export instead.');
|
|
572
|
+
} catch (error) {
|
|
573
|
+
console.error('STEP export error:', error);
|
|
574
|
+
alert(`STEP export failed: ${error.message}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Export features as cycleCAD native JSON format
|
|
580
|
+
* This format preserves all feature parameters for re-opening and editing
|
|
581
|
+
* @param {Array} features - Array of feature objects with type, params, etc.
|
|
582
|
+
* @param {string} filename - Output filename
|
|
583
|
+
*/
|
|
584
|
+
export function exportJSON(features, filename = 'model.cyclecad.json') {
|
|
585
|
+
const exportData = {
|
|
586
|
+
version: '0.1',
|
|
587
|
+
timestamp: new Date().toISOString(),
|
|
588
|
+
features: features.map(feature => ({
|
|
589
|
+
id: feature.id || `feature_${Math.random().toString(36).substr(2, 9)}`,
|
|
590
|
+
type: feature.type, // 'box', 'sphere', 'cylinder', 'sketch', etc.
|
|
591
|
+
name: feature.name || feature.type,
|
|
592
|
+
visible: feature.visible !== false,
|
|
593
|
+
params: feature.params || {}, // Feature-specific parameters
|
|
594
|
+
sketch: feature.sketch || null, // Sketch data if applicable
|
|
595
|
+
operations: feature.operations || [], // Applied operations (fillet, chamfer, etc.)
|
|
596
|
+
material: feature.material || 'default',
|
|
597
|
+
metadata: {
|
|
598
|
+
created: feature.created || new Date().toISOString(),
|
|
599
|
+
modified: new Date().toISOString()
|
|
600
|
+
}
|
|
601
|
+
}))
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const jsonContent = JSON.stringify(exportData, null, 2);
|
|
605
|
+
downloadFile(jsonContent, filename, 'application/json');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Import cycleCAD native JSON format
|
|
610
|
+
* @param {string} jsonString - JSON string to parse
|
|
611
|
+
* @returns {Object} Parsed data with version and features array
|
|
612
|
+
*/
|
|
613
|
+
export function importJSON(jsonString) {
|
|
614
|
+
try {
|
|
615
|
+
const data = JSON.parse(jsonString);
|
|
616
|
+
|
|
617
|
+
// Validate structure
|
|
618
|
+
if (!data.version || !Array.isArray(data.features)) {
|
|
619
|
+
throw new Error('Invalid cycleCAD JSON format. Expected { version, features }');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Version compatibility check
|
|
623
|
+
const majorVersion = parseInt(data.version.split('.')[0]);
|
|
624
|
+
if (majorVersion > 0) {
|
|
625
|
+
console.warn(`JSON version ${data.version} may have compatibility issues`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return data;
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error('JSON import error:', error);
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Batch export all formats
|
|
637
|
+
* Exports model in STL, OBJ, and glTF formats simultaneously
|
|
638
|
+
* @param {Array} features - Array of feature objects
|
|
639
|
+
* @param {string} baseName - Base filename without extension
|
|
640
|
+
*/
|
|
641
|
+
export function exportAllFormats(features, baseName = 'model') {
|
|
642
|
+
exportSTL(features, `${baseName}.stl`);
|
|
643
|
+
exportOBJ(features, `${baseName}.obj`);
|
|
644
|
+
exportGLTF(features, `${baseName}.gltf`);
|
|
645
|
+
exportJSON(features, `${baseName}.cyclecad.json`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export default {
|
|
649
|
+
downloadFile,
|
|
650
|
+
exportSTL,
|
|
651
|
+
exportSTLBinary,
|
|
652
|
+
exportOBJ,
|
|
653
|
+
exportGLTF,
|
|
654
|
+
exportSTEP,
|
|
655
|
+
exportJSON,
|
|
656
|
+
importJSON,
|
|
657
|
+
exportAllFormats
|
|
658
|
+
};
|