cyclecad 0.1.4 → 0.1.7
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/CLAUDE.md +20 -9
- package/app/index.html +451 -3
- package/app/js/advanced-ops.js +762 -0
- package/app/js/assembly.js +1102 -0
- package/app/js/constraint-solver.js +1046 -0
- package/app/js/dxf-export.js +1173 -0
- package/app/js/viewport.js +83 -0
- package/app/mobile.html +1276 -0
- package/package.json +1 -1
- package/DUO-MANIFEST-README.md +0 -233
- package/app/duo-manifest-demo.html +0 -337
- package/app/duo-manifest.json +0 -7375
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DXF Export Module for cycleCAD
|
|
3
|
+
*
|
|
4
|
+
* Exports 2D sketches and 3D model projections to AutoCAD-compatible DXF files.
|
|
5
|
+
* Supports ASCII DXF R12/R14 format for maximum compatibility.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const dxfContent = exportSketchToDXF(sketchEntities, { units: 'mm', filename: 'part.dxf' });
|
|
9
|
+
* downloadDXF(dxfContent, 'part.dxf');
|
|
10
|
+
*
|
|
11
|
+
* const dxfContent = exportProjectionToDXF(mesh, 'front', { hiddenLines: true });
|
|
12
|
+
* downloadDXF(dxfContent, 'projection.dxf');
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// DXF Constants & Utilities
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
const DXF_HEADER = {
|
|
22
|
+
VERSION: 'AC1012', // AutoCAD R13 (most compatible)
|
|
23
|
+
INSUNITS: 4, // 4 = millimeters
|
|
24
|
+
EXTMIN: { x: 0, y: 0, z: 0 },
|
|
25
|
+
EXTMAX: { x: 100, y: 100, z: 0 }
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DXF_LAYERS = {
|
|
29
|
+
OUTLINE: { name: 'OUTLINE', color: 7, linetype: 'CONTINUOUS' }, // White
|
|
30
|
+
HIDDEN: { name: 'HIDDEN', color: 2, linetype: 'HIDDEN' }, // Yellow/dashed
|
|
31
|
+
CENTER: { name: 'CENTER', color: 1, linetype: 'CENTER' }, // Red/dash-dot
|
|
32
|
+
DIMENSION: { name: 'DIMENSION', color: 3, linetype: 'CONTINUOUS' }, // Green
|
|
33
|
+
BORDER: { name: 'BORDER', color: 7, linetype: 'CONTINUOUS' }, // White
|
|
34
|
+
MODEL: { name: '0', color: 7, linetype: 'CONTINUOUS' } // Default layer
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const DXF_COLORS = {
|
|
38
|
+
WHITE: 7,
|
|
39
|
+
YELLOW: 2,
|
|
40
|
+
RED: 1,
|
|
41
|
+
GREEN: 3,
|
|
42
|
+
BLUE: 5,
|
|
43
|
+
MAGENTA: 6,
|
|
44
|
+
CYAN: 4
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// DXF Section Builders
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build DXF HEADER section
|
|
53
|
+
* @param {Object} extents - { min: {x,y,z}, max: {x,y,z} }
|
|
54
|
+
* @returns {string} DXF header text
|
|
55
|
+
*/
|
|
56
|
+
function buildHeaderSection(extents) {
|
|
57
|
+
const lines = [];
|
|
58
|
+
lines.push(' 0');
|
|
59
|
+
lines.push('SECTION');
|
|
60
|
+
lines.push(' 2');
|
|
61
|
+
lines.push('HEADER');
|
|
62
|
+
|
|
63
|
+
// DXF version
|
|
64
|
+
lines.push(' 9');
|
|
65
|
+
lines.push('$ACADVER');
|
|
66
|
+
lines.push(' 1');
|
|
67
|
+
lines.push('AC1014'); // R14 format
|
|
68
|
+
|
|
69
|
+
// Drawing extents
|
|
70
|
+
lines.push(' 9');
|
|
71
|
+
lines.push('$EXTMIN');
|
|
72
|
+
lines.push(' 10');
|
|
73
|
+
lines.push(formatNumber(extents.min.x));
|
|
74
|
+
lines.push(' 20');
|
|
75
|
+
lines.push(formatNumber(extents.min.y));
|
|
76
|
+
|
|
77
|
+
lines.push(' 9');
|
|
78
|
+
lines.push('$EXTMAX');
|
|
79
|
+
lines.push(' 10');
|
|
80
|
+
lines.push(formatNumber(extents.max.x));
|
|
81
|
+
lines.push(' 20');
|
|
82
|
+
lines.push(formatNumber(extents.max.y));
|
|
83
|
+
|
|
84
|
+
// Units in millimeters
|
|
85
|
+
lines.push(' 9');
|
|
86
|
+
lines.push('$INSUNITS');
|
|
87
|
+
lines.push(' 70');
|
|
88
|
+
lines.push('4');
|
|
89
|
+
|
|
90
|
+
// Default text height
|
|
91
|
+
lines.push(' 9');
|
|
92
|
+
lines.push('$TEXTSIZE');
|
|
93
|
+
lines.push(' 40');
|
|
94
|
+
lines.push('2.5');
|
|
95
|
+
|
|
96
|
+
lines.push(' 0');
|
|
97
|
+
lines.push('ENDSEC');
|
|
98
|
+
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build DXF TABLES section (layers, line types, text styles)
|
|
104
|
+
* @returns {string} DXF tables text
|
|
105
|
+
*/
|
|
106
|
+
function buildTablesSection() {
|
|
107
|
+
const lines = [];
|
|
108
|
+
lines.push(' 0');
|
|
109
|
+
lines.push('SECTION');
|
|
110
|
+
lines.push(' 2');
|
|
111
|
+
lines.push('TABLES');
|
|
112
|
+
|
|
113
|
+
// VPORT table (required)
|
|
114
|
+
lines.push(' 0');
|
|
115
|
+
lines.push('TABLE');
|
|
116
|
+
lines.push(' 2');
|
|
117
|
+
lines.push('VPORT');
|
|
118
|
+
lines.push(' 70');
|
|
119
|
+
lines.push('1');
|
|
120
|
+
lines.push(' 0');
|
|
121
|
+
lines.push('VPORT');
|
|
122
|
+
lines.push(' 2');
|
|
123
|
+
lines.push('*ACTIVE');
|
|
124
|
+
lines.push(' 70');
|
|
125
|
+
lines.push('0');
|
|
126
|
+
lines.push(' 10');
|
|
127
|
+
lines.push('0.0');
|
|
128
|
+
lines.push(' 20');
|
|
129
|
+
lines.push('0.0');
|
|
130
|
+
lines.push(' 11');
|
|
131
|
+
lines.push('1.0');
|
|
132
|
+
lines.push(' 21');
|
|
133
|
+
lines.push('1.0');
|
|
134
|
+
lines.push(' 0');
|
|
135
|
+
lines.push('ENDTAB');
|
|
136
|
+
|
|
137
|
+
// LTYPE table (line types)
|
|
138
|
+
lines.push(' 0');
|
|
139
|
+
lines.push('TABLE');
|
|
140
|
+
lines.push(' 2');
|
|
141
|
+
lines.push('LTYPE');
|
|
142
|
+
lines.push(' 70');
|
|
143
|
+
lines.push('4');
|
|
144
|
+
|
|
145
|
+
// CONTINUOUS
|
|
146
|
+
lines.push(' 0');
|
|
147
|
+
lines.push('LTYPE');
|
|
148
|
+
lines.push(' 2');
|
|
149
|
+
lines.push('CONTINUOUS');
|
|
150
|
+
lines.push(' 70');
|
|
151
|
+
lines.push('0');
|
|
152
|
+
lines.push(' 3');
|
|
153
|
+
lines.push('Solid line');
|
|
154
|
+
lines.push(' 72');
|
|
155
|
+
lines.push('0');
|
|
156
|
+
lines.push(' 73');
|
|
157
|
+
lines.push('0');
|
|
158
|
+
lines.push(' 40');
|
|
159
|
+
lines.push('0.0');
|
|
160
|
+
|
|
161
|
+
// HIDDEN (dashed)
|
|
162
|
+
lines.push(' 0');
|
|
163
|
+
lines.push('LTYPE');
|
|
164
|
+
lines.push(' 2');
|
|
165
|
+
lines.push('HIDDEN');
|
|
166
|
+
lines.push(' 70');
|
|
167
|
+
lines.push('0');
|
|
168
|
+
lines.push(' 3');
|
|
169
|
+
lines.push('Hidden line');
|
|
170
|
+
lines.push(' 72');
|
|
171
|
+
lines.push('1');
|
|
172
|
+
lines.push(' 73');
|
|
173
|
+
lines.push('1');
|
|
174
|
+
lines.push(' 40');
|
|
175
|
+
lines.push('9.525');
|
|
176
|
+
lines.push(' 49');
|
|
177
|
+
lines.push('4.7625');
|
|
178
|
+
lines.push(' 49');
|
|
179
|
+
lines.push('-4.7625');
|
|
180
|
+
|
|
181
|
+
// CENTER (dash-dot)
|
|
182
|
+
lines.push(' 0');
|
|
183
|
+
lines.push('LTYPE');
|
|
184
|
+
lines.push(' 2');
|
|
185
|
+
lines.push('CENTER');
|
|
186
|
+
lines.push(' 70');
|
|
187
|
+
lines.push('0');
|
|
188
|
+
lines.push(' 3');
|
|
189
|
+
lines.push('Center line');
|
|
190
|
+
lines.push(' 72');
|
|
191
|
+
lines.push('1');
|
|
192
|
+
lines.push(' 73');
|
|
193
|
+
lines.push('2');
|
|
194
|
+
lines.push(' 40');
|
|
195
|
+
lines.push('20.0');
|
|
196
|
+
lines.push(' 49');
|
|
197
|
+
lines.push('12.5');
|
|
198
|
+
lines.push(' 49');
|
|
199
|
+
lines.push('-2.5');
|
|
200
|
+
lines.push(' 49');
|
|
201
|
+
lines.push('2.5');
|
|
202
|
+
lines.push(' 49');
|
|
203
|
+
lines.push('-2.5');
|
|
204
|
+
|
|
205
|
+
lines.push(' 0');
|
|
206
|
+
lines.push('ENDTAB');
|
|
207
|
+
|
|
208
|
+
// LAYER table
|
|
209
|
+
lines.push(' 0');
|
|
210
|
+
lines.push('TABLE');
|
|
211
|
+
lines.push(' 2');
|
|
212
|
+
lines.push('LAYER');
|
|
213
|
+
lines.push(' 70');
|
|
214
|
+
lines.push('6');
|
|
215
|
+
|
|
216
|
+
Object.values(DXF_LAYERS).forEach(layer => {
|
|
217
|
+
lines.push(' 0');
|
|
218
|
+
lines.push('LAYER');
|
|
219
|
+
lines.push(' 2');
|
|
220
|
+
lines.push(layer.name);
|
|
221
|
+
lines.push(' 70');
|
|
222
|
+
lines.push('0');
|
|
223
|
+
lines.push(' 62');
|
|
224
|
+
lines.push(layer.color.toString());
|
|
225
|
+
lines.push(' 6');
|
|
226
|
+
lines.push(layer.linetype);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
lines.push(' 0');
|
|
230
|
+
lines.push('ENDTAB');
|
|
231
|
+
|
|
232
|
+
// STYLE table
|
|
233
|
+
lines.push(' 0');
|
|
234
|
+
lines.push('TABLE');
|
|
235
|
+
lines.push(' 2');
|
|
236
|
+
lines.push('STYLE');
|
|
237
|
+
lines.push(' 70');
|
|
238
|
+
lines.push('1');
|
|
239
|
+
lines.push(' 0');
|
|
240
|
+
lines.push('STYLE');
|
|
241
|
+
lines.push(' 2');
|
|
242
|
+
lines.push('STANDARD');
|
|
243
|
+
lines.push(' 70');
|
|
244
|
+
lines.push('0');
|
|
245
|
+
lines.push(' 40');
|
|
246
|
+
lines.push('0.0');
|
|
247
|
+
lines.push(' 41');
|
|
248
|
+
lines.push('1.0');
|
|
249
|
+
lines.push(' 50');
|
|
250
|
+
lines.push('0.0');
|
|
251
|
+
lines.push(' 71');
|
|
252
|
+
lines.push('0');
|
|
253
|
+
lines.push(' 3');
|
|
254
|
+
lines.push('txt');
|
|
255
|
+
lines.push(' 0');
|
|
256
|
+
lines.push('ENDTAB');
|
|
257
|
+
|
|
258
|
+
lines.push(' 0');
|
|
259
|
+
lines.push('ENDSEC');
|
|
260
|
+
|
|
261
|
+
return lines.join('\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Build DXF BLOCKS section
|
|
266
|
+
* @returns {string} DXF blocks text
|
|
267
|
+
*/
|
|
268
|
+
function buildBlocksSection() {
|
|
269
|
+
const lines = [];
|
|
270
|
+
lines.push(' 0');
|
|
271
|
+
lines.push('SECTION');
|
|
272
|
+
lines.push(' 2');
|
|
273
|
+
lines.push('BLOCKS');
|
|
274
|
+
|
|
275
|
+
// Default block
|
|
276
|
+
lines.push(' 0');
|
|
277
|
+
lines.push('BLOCK');
|
|
278
|
+
lines.push(' 8');
|
|
279
|
+
lines.push('0');
|
|
280
|
+
lines.push(' 2');
|
|
281
|
+
lines.push('*MODEL_SPACE');
|
|
282
|
+
lines.push(' 70');
|
|
283
|
+
lines.push('0');
|
|
284
|
+
lines.push(' 10');
|
|
285
|
+
lines.push('0.0');
|
|
286
|
+
lines.push(' 20');
|
|
287
|
+
lines.push('0.0');
|
|
288
|
+
lines.push(' 0');
|
|
289
|
+
lines.push('ENDBLK');
|
|
290
|
+
|
|
291
|
+
lines.push(' 0');
|
|
292
|
+
lines.push('ENDSEC');
|
|
293
|
+
|
|
294
|
+
return lines.join('\n');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ============================================================================
|
|
298
|
+
// Core Export Functions
|
|
299
|
+
// ============================================================================
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Export 2D sketch entities to DXF format
|
|
303
|
+
*
|
|
304
|
+
* @param {Array<Object>} entities - Sketch entities
|
|
305
|
+
* Each entity: { type: 'line'|'rectangle'|'circle'|'arc'|'polyline', points: [{x,y}...], dimensions: {} }
|
|
306
|
+
* @param {Object} options - Export options
|
|
307
|
+
* @param {string} options.units - Unit system ('mm', 'in', 'cm') - default 'mm'
|
|
308
|
+
* @param {boolean} options.layers - Create separate layers - default true
|
|
309
|
+
* @param {boolean} options.dimensions - Export dimension annotations - default true
|
|
310
|
+
* @param {string} options.filename - Output filename - default 'sketch.dxf'
|
|
311
|
+
* @returns {string} DXF file content (ASCII)
|
|
312
|
+
*/
|
|
313
|
+
export function exportSketchToDXF(entities, options = {}) {
|
|
314
|
+
const opts = {
|
|
315
|
+
units: 'mm',
|
|
316
|
+
layers: true,
|
|
317
|
+
dimensions: true,
|
|
318
|
+
filename: 'sketch.dxf',
|
|
319
|
+
...options
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Calculate extents
|
|
323
|
+
const extents = calculateExtents(entities);
|
|
324
|
+
|
|
325
|
+
// Build sections
|
|
326
|
+
const header = buildHeaderSection(extents);
|
|
327
|
+
const tables = buildTablesSection();
|
|
328
|
+
const blocks = buildBlocksSection();
|
|
329
|
+
|
|
330
|
+
// Build ENTITIES section
|
|
331
|
+
const entitiesLines = [];
|
|
332
|
+
entitiesLines.push(' 0');
|
|
333
|
+
entitiesLines.push('SECTION');
|
|
334
|
+
entitiesLines.push(' 2');
|
|
335
|
+
entitiesLines.push('ENTITIES');
|
|
336
|
+
|
|
337
|
+
// Convert sketch entities to DXF entities
|
|
338
|
+
entities.forEach((entity, idx) => {
|
|
339
|
+
const layer = opts.layers ? `ENTITY_${idx}` : DXF_LAYERS.OUTLINE.name;
|
|
340
|
+
|
|
341
|
+
switch (entity.type.toLowerCase()) {
|
|
342
|
+
case 'line':
|
|
343
|
+
entitiesLines.push(...createDXFLine(entity.points[0], entity.points[1], layer));
|
|
344
|
+
break;
|
|
345
|
+
case 'rectangle':
|
|
346
|
+
entitiesLines.push(...createDXFRectangle(entity.points, layer));
|
|
347
|
+
break;
|
|
348
|
+
case 'circle':
|
|
349
|
+
entitiesLines.push(...createDXFCircle(entity.points[0], entity.dimensions?.radius || 10, layer));
|
|
350
|
+
break;
|
|
351
|
+
case 'arc':
|
|
352
|
+
entitiesLines.push(...createDXFArc(entity.points[0], entity.dimensions?.radius || 10,
|
|
353
|
+
entity.dimensions?.startAngle || 0, entity.dimensions?.endAngle || 180, layer));
|
|
354
|
+
break;
|
|
355
|
+
case 'polyline':
|
|
356
|
+
entitiesLines.push(...createDXFPolyline(entity.points, layer));
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Add dimension annotations if requested
|
|
362
|
+
if (opts.dimensions && entities.some(e => e.dimensions)) {
|
|
363
|
+
entities.forEach((entity, idx) => {
|
|
364
|
+
if (entity.dimensions?.label) {
|
|
365
|
+
const midpoint = calculateMidpoint(entity.points);
|
|
366
|
+
entitiesLines.push(...createDXFText(midpoint, entity.dimensions.label, 2.5, DXF_LAYERS.DIMENSION.name));
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
entitiesLines.push(' 0');
|
|
372
|
+
entitiesLines.push('ENDSEC');
|
|
373
|
+
|
|
374
|
+
// Build EOF
|
|
375
|
+
const eof = [' 0', 'EOF'];
|
|
376
|
+
|
|
377
|
+
return [header, tables, blocks, entitiesLines.join('\n'), eof.join('\n')].join('\n');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Export 3D mesh projection to DXF format
|
|
382
|
+
*
|
|
383
|
+
* @param {THREE.Mesh|THREE.Group} mesh - Geometry to project
|
|
384
|
+
* @param {string} view - Projection view: 'front'|'top'|'right'|'back'|'left'|'bottom'|'iso'
|
|
385
|
+
* @param {Object} options - Export options
|
|
386
|
+
* @param {boolean} options.hiddenLines - Separate hidden lines to layer - default true
|
|
387
|
+
* @param {number} options.scale - Scaling factor - default 1.0
|
|
388
|
+
* @param {boolean} options.layers - Use separate layers - default true
|
|
389
|
+
* @returns {string} DXF file content
|
|
390
|
+
*/
|
|
391
|
+
export function exportProjectionToDXF(mesh, view = 'front', options = {}) {
|
|
392
|
+
const opts = {
|
|
393
|
+
hiddenLines: true,
|
|
394
|
+
scale: 1.0,
|
|
395
|
+
layers: true,
|
|
396
|
+
...options
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Get projection matrix for view
|
|
400
|
+
const projMatrix = getProjectionMatrix(view);
|
|
401
|
+
|
|
402
|
+
// Extract visible and hidden edges
|
|
403
|
+
const visibleEdges = extractProjectedEdges(mesh, projMatrix, opts.scale, true);
|
|
404
|
+
const hiddenEdges = opts.hiddenLines ? extractProjectedEdges(mesh, projMatrix, opts.scale, false) : [];
|
|
405
|
+
|
|
406
|
+
// Calculate extents
|
|
407
|
+
const allEdges = [...visibleEdges, ...hiddenEdges];
|
|
408
|
+
const extents = calculateExtentsFromEdges(allEdges);
|
|
409
|
+
|
|
410
|
+
// Build sections
|
|
411
|
+
const header = buildHeaderSection(extents);
|
|
412
|
+
const tables = buildTablesSection();
|
|
413
|
+
const blocks = buildBlocksSection();
|
|
414
|
+
|
|
415
|
+
// Build ENTITIES section
|
|
416
|
+
const entitiesLines = [];
|
|
417
|
+
entitiesLines.push(' 0');
|
|
418
|
+
entitiesLines.push('SECTION');
|
|
419
|
+
entitiesLines.push(' 2');
|
|
420
|
+
entitiesLines.push('ENTITIES');
|
|
421
|
+
|
|
422
|
+
// Add visible edges
|
|
423
|
+
visibleEdges.forEach(edge => {
|
|
424
|
+
entitiesLines.push(...createDXFLine(edge.start, edge.end, DXF_LAYERS.OUTLINE.name));
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Add hidden edges on separate layer
|
|
428
|
+
if (opts.hiddenLines) {
|
|
429
|
+
hiddenEdges.forEach(edge => {
|
|
430
|
+
entitiesLines.push(...createDXFLine(edge.start, edge.end, DXF_LAYERS.HIDDEN.name));
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
entitiesLines.push(' 0');
|
|
435
|
+
entitiesLines.push('ENDSEC');
|
|
436
|
+
|
|
437
|
+
const eof = [' 0', 'EOF'];
|
|
438
|
+
|
|
439
|
+
return [header, tables, blocks, entitiesLines.join('\n'), eof.join('\n')].join('\n');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Export standard engineering multi-view drawing
|
|
444
|
+
* Creates front, top, right views in 3rd-angle projection layout
|
|
445
|
+
*
|
|
446
|
+
* @param {THREE.Mesh|THREE.Group} mesh - Geometry to project
|
|
447
|
+
* @param {Object} options - Export options
|
|
448
|
+
* @param {number} options.scale - View scale - default 1.0
|
|
449
|
+
* @param {number} options.spacing - Space between views (mm) - default 50
|
|
450
|
+
* @param {boolean} options.border - Draw border - default true
|
|
451
|
+
* @param {boolean} options.titleBlock - Draw title block - default true
|
|
452
|
+
* @param {string} options.title - Drawing title - default 'Part'
|
|
453
|
+
* @param {string} options.partNumber - Part number - default ''
|
|
454
|
+
* @param {string} options.material - Material description - default ''
|
|
455
|
+
* @param {string} options.author - Author name - default ''
|
|
456
|
+
* @returns {string} DXF file content
|
|
457
|
+
*/
|
|
458
|
+
export function exportMultiViewDXF(mesh, options = {}) {
|
|
459
|
+
const opts = {
|
|
460
|
+
scale: 1.0,
|
|
461
|
+
spacing: 50,
|
|
462
|
+
border: true,
|
|
463
|
+
titleBlock: true,
|
|
464
|
+
title: 'Part',
|
|
465
|
+
partNumber: '',
|
|
466
|
+
material: '',
|
|
467
|
+
author: '',
|
|
468
|
+
...options
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// Extract projections
|
|
472
|
+
const frontProj = extractProjectedEdges(mesh, getProjectionMatrix('front'), opts.scale, true);
|
|
473
|
+
const topProj = extractProjectedEdges(mesh, getProjectionMatrix('top'), opts.scale, true);
|
|
474
|
+
const rightProj = extractProjectedEdges(mesh, getProjectionMatrix('right'), opts.scale, true);
|
|
475
|
+
|
|
476
|
+
// Calculate view extents
|
|
477
|
+
const frontExt = calculateExtentsFromEdges(frontProj);
|
|
478
|
+
const topExt = calculateExtentsFromEdges(topProj);
|
|
479
|
+
const rightExt = calculateExtentsFromEdges(rightProj);
|
|
480
|
+
|
|
481
|
+
// Layout views (3rd angle projection)
|
|
482
|
+
const layout = {
|
|
483
|
+
front: { x: 10, y: 10, width: frontExt.max.x - frontExt.min.x, height: frontExt.max.y - frontExt.min.y },
|
|
484
|
+
top: { x: 10, y: 10 + frontExt.max.y - frontExt.min.y + opts.spacing, width: topExt.max.x - topExt.min.x, height: topExt.max.y - topExt.min.y },
|
|
485
|
+
right: { x: 10 + frontExt.max.x - frontExt.min.x + opts.spacing, y: 10, width: rightExt.max.x - rightExt.min.x, height: rightExt.max.y - rightExt.min.y }
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// Calculate total extents for border
|
|
489
|
+
const totalWidth = layout.front.width + layout.right.width + opts.spacing + 30;
|
|
490
|
+
const totalHeight = layout.front.height + layout.top.height + opts.spacing + (opts.titleBlock ? 60 : 30);
|
|
491
|
+
const overallExtents = { min: { x: 0, y: 0 }, max: { x: totalWidth, y: totalHeight } };
|
|
492
|
+
|
|
493
|
+
// Build DXF
|
|
494
|
+
const header = buildHeaderSection(overallExtents);
|
|
495
|
+
const tables = buildTablesSection();
|
|
496
|
+
const blocks = buildBlocksSection();
|
|
497
|
+
|
|
498
|
+
const entitiesLines = [];
|
|
499
|
+
entitiesLines.push(' 0');
|
|
500
|
+
entitiesLines.push('SECTION');
|
|
501
|
+
entitiesLines.push(' 2');
|
|
502
|
+
entitiesLines.push('ENTITIES');
|
|
503
|
+
|
|
504
|
+
// Draw border
|
|
505
|
+
if (opts.border) {
|
|
506
|
+
entitiesLines.push(...createDXFRectangle(
|
|
507
|
+
[
|
|
508
|
+
{ x: 5, y: 5 },
|
|
509
|
+
{ x: totalWidth - 5, y: totalHeight - 5 }
|
|
510
|
+
],
|
|
511
|
+
DXF_LAYERS.BORDER.name
|
|
512
|
+
));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Draw title block
|
|
516
|
+
if (opts.titleBlock) {
|
|
517
|
+
const titleY = 20;
|
|
518
|
+
entitiesLines.push(...createDXFText({ x: totalWidth - 100, y: titleY }, opts.title, 3.5, DXF_LAYERS.BORDER.name));
|
|
519
|
+
if (opts.partNumber) {
|
|
520
|
+
entitiesLines.push(...createDXFText({ x: totalWidth - 100, y: titleY - 8 }, `Part: ${opts.partNumber}`, 2, DXF_LAYERS.BORDER.name));
|
|
521
|
+
}
|
|
522
|
+
if (opts.material) {
|
|
523
|
+
entitiesLines.push(...createDXFText({ x: totalWidth - 100, y: titleY - 14 }, `Material: ${opts.material}`, 2, DXF_LAYERS.BORDER.name));
|
|
524
|
+
}
|
|
525
|
+
if (opts.author) {
|
|
526
|
+
entitiesLines.push(...createDXFText({ x: totalWidth - 100, y: titleY - 20 }, `By: ${opts.author}`, 2, DXF_LAYERS.BORDER.name));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Draw front view
|
|
531
|
+
entitiesLines.push(...createDXFText({ x: layout.front.x, y: layout.front.y - 5 }, 'FRONT', 2.5, DXF_LAYERS.DIMENSION.name));
|
|
532
|
+
frontProj.forEach(edge => {
|
|
533
|
+
entitiesLines.push(...createDXFLine(
|
|
534
|
+
{ x: edge.start.x + layout.front.x, y: edge.start.y + layout.front.y },
|
|
535
|
+
{ x: edge.end.x + layout.front.x, y: edge.end.y + layout.front.y },
|
|
536
|
+
DXF_LAYERS.OUTLINE.name
|
|
537
|
+
));
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Draw top view
|
|
541
|
+
entitiesLines.push(...createDXFText({ x: layout.top.x, y: layout.top.y - 5 }, 'TOP', 2.5, DXF_LAYERS.DIMENSION.name));
|
|
542
|
+
topProj.forEach(edge => {
|
|
543
|
+
entitiesLines.push(...createDXFLine(
|
|
544
|
+
{ x: edge.start.x + layout.top.x, y: edge.start.y + layout.top.y },
|
|
545
|
+
{ x: edge.end.x + layout.top.x, y: edge.end.y + layout.top.y },
|
|
546
|
+
DXF_LAYERS.OUTLINE.name
|
|
547
|
+
));
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Draw right view
|
|
551
|
+
entitiesLines.push(...createDXFText({ x: layout.right.x, y: layout.right.y - 5 }, 'RIGHT', 2.5, DXF_LAYERS.DIMENSION.name));
|
|
552
|
+
rightProj.forEach(edge => {
|
|
553
|
+
entitiesLines.push(...createDXFLine(
|
|
554
|
+
{ x: edge.start.x + layout.right.x, y: edge.start.y + layout.right.y },
|
|
555
|
+
{ x: edge.end.x + layout.right.x, y: edge.end.y + layout.right.y },
|
|
556
|
+
DXF_LAYERS.OUTLINE.name
|
|
557
|
+
));
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
entitiesLines.push(' 0');
|
|
561
|
+
entitiesLines.push('ENDSEC');
|
|
562
|
+
|
|
563
|
+
const eof = [' 0', 'EOF'];
|
|
564
|
+
|
|
565
|
+
return [header, tables, blocks, entitiesLines.join('\n'), eof.join('\n')].join('\n');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Export 3D geometry as wireframe to DXF
|
|
570
|
+
*
|
|
571
|
+
* @param {THREE.Mesh|THREE.Group} mesh - Geometry to export
|
|
572
|
+
* @param {Object} options - Export options
|
|
573
|
+
* @param {boolean} options.faces - Export faces as 3DFACE - default true
|
|
574
|
+
* @param {boolean} options.edges - Export edges as 3DLINE - default true
|
|
575
|
+
* @param {string} options.layer - Layer name - default 'MODEL'
|
|
576
|
+
* @returns {string} DXF file content
|
|
577
|
+
*/
|
|
578
|
+
export function export3DDXF(mesh, options = {}) {
|
|
579
|
+
const opts = {
|
|
580
|
+
faces: true,
|
|
581
|
+
edges: true,
|
|
582
|
+
layer: DXF_LAYERS.MODEL.name,
|
|
583
|
+
...options
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// Extract all unique vertices and faces
|
|
587
|
+
const vertices = [];
|
|
588
|
+
const faces = [];
|
|
589
|
+
|
|
590
|
+
extractGeometry(mesh, vertices, faces);
|
|
591
|
+
|
|
592
|
+
// Calculate extents in 3D
|
|
593
|
+
const extents = { min: { x: 0, y: 0, z: 0 }, max: { x: 100, y: 100, z: 100 } };
|
|
594
|
+
if (vertices.length > 0) {
|
|
595
|
+
extents.min = { x: vertices[0].x, y: vertices[0].y, z: vertices[0].z };
|
|
596
|
+
extents.max = { ...extents.min };
|
|
597
|
+
vertices.forEach(v => {
|
|
598
|
+
extents.min.x = Math.min(extents.min.x, v.x);
|
|
599
|
+
extents.min.y = Math.min(extents.min.y, v.y);
|
|
600
|
+
extents.min.z = Math.min(extents.min.z, v.z);
|
|
601
|
+
extents.max.x = Math.max(extents.max.x, v.x);
|
|
602
|
+
extents.max.y = Math.max(extents.max.y, v.y);
|
|
603
|
+
extents.max.z = Math.max(extents.max.z, v.z);
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Build DXF
|
|
608
|
+
const header = buildHeaderSection(extents);
|
|
609
|
+
const tables = buildTablesSection();
|
|
610
|
+
const blocks = buildBlocksSection();
|
|
611
|
+
|
|
612
|
+
const entitiesLines = [];
|
|
613
|
+
entitiesLines.push(' 0');
|
|
614
|
+
entitiesLines.push('SECTION');
|
|
615
|
+
entitiesLines.push(' 2');
|
|
616
|
+
entitiesLines.push('ENTITIES');
|
|
617
|
+
|
|
618
|
+
// Export 3D faces
|
|
619
|
+
if (opts.faces) {
|
|
620
|
+
faces.forEach(face => {
|
|
621
|
+
entitiesLines.push(...createDXF3DFace(face, opts.layer));
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Export edges
|
|
626
|
+
if (opts.edges) {
|
|
627
|
+
const edges = extractEdges(faces);
|
|
628
|
+
edges.forEach(edge => {
|
|
629
|
+
entitiesLines.push(...createDXF3DLine(edge.start, edge.end, opts.layer));
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
entitiesLines.push(' 0');
|
|
634
|
+
entitiesLines.push('ENDSEC');
|
|
635
|
+
|
|
636
|
+
const eof = [' 0', 'EOF'];
|
|
637
|
+
|
|
638
|
+
return [header, tables, blocks, entitiesLines.join('\n'), eof.join('\n')].join('\n');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ============================================================================
|
|
642
|
+
// DXF Entity Creators
|
|
643
|
+
// ============================================================================
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Create DXF LINE entity
|
|
647
|
+
* @private
|
|
648
|
+
*/
|
|
649
|
+
function createDXFLine(p1, p2, layer = '0') {
|
|
650
|
+
return [
|
|
651
|
+
' 0', 'LINE',
|
|
652
|
+
' 8', layer,
|
|
653
|
+
' 10', formatNumber(p1.x),
|
|
654
|
+
' 20', formatNumber(p1.y),
|
|
655
|
+
' 30', '0.0',
|
|
656
|
+
' 11', formatNumber(p2.x),
|
|
657
|
+
' 21', formatNumber(p2.y),
|
|
658
|
+
' 31', '0.0'
|
|
659
|
+
];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Create DXF CIRCLE entity
|
|
664
|
+
* @private
|
|
665
|
+
*/
|
|
666
|
+
function createDXFCircle(center, radius, layer = '0') {
|
|
667
|
+
return [
|
|
668
|
+
' 0', 'CIRCLE',
|
|
669
|
+
' 8', layer,
|
|
670
|
+
' 10', formatNumber(center.x),
|
|
671
|
+
' 20', formatNumber(center.y),
|
|
672
|
+
' 30', '0.0',
|
|
673
|
+
' 40', formatNumber(radius)
|
|
674
|
+
];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Create DXF ARC entity
|
|
679
|
+
* @private
|
|
680
|
+
*/
|
|
681
|
+
function createDXFArc(center, radius, startAngle, endAngle, layer = '0') {
|
|
682
|
+
return [
|
|
683
|
+
' 0', 'ARC',
|
|
684
|
+
' 8', layer,
|
|
685
|
+
' 10', formatNumber(center.x),
|
|
686
|
+
' 20', formatNumber(center.y),
|
|
687
|
+
' 30', '0.0',
|
|
688
|
+
' 40', formatNumber(radius),
|
|
689
|
+
' 50', formatNumber(startAngle),
|
|
690
|
+
' 51', formatNumber(endAngle)
|
|
691
|
+
];
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Create DXF LWPOLYLINE entity
|
|
696
|
+
* @private
|
|
697
|
+
*/
|
|
698
|
+
function createDXFPolyline(points, layer = '0') {
|
|
699
|
+
const lines = [
|
|
700
|
+
' 0', 'LWPOLYLINE',
|
|
701
|
+
' 8', layer,
|
|
702
|
+
' 70', '0',
|
|
703
|
+
' 90', points.length.toString()
|
|
704
|
+
];
|
|
705
|
+
|
|
706
|
+
points.forEach(p => {
|
|
707
|
+
lines.push(' 10');
|
|
708
|
+
lines.push(formatNumber(p.x));
|
|
709
|
+
lines.push(' 20');
|
|
710
|
+
lines.push(formatNumber(p.y));
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
return lines;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Create DXF TEXT entity
|
|
718
|
+
* @private
|
|
719
|
+
*/
|
|
720
|
+
function createDXFText(position, text, height = 2.5, layer = '0') {
|
|
721
|
+
return [
|
|
722
|
+
' 0', 'TEXT',
|
|
723
|
+
' 8', layer,
|
|
724
|
+
' 10', formatNumber(position.x),
|
|
725
|
+
' 20', formatNumber(position.y),
|
|
726
|
+
' 30', '0.0',
|
|
727
|
+
' 40', formatNumber(height),
|
|
728
|
+
' 1', text,
|
|
729
|
+
' 7', 'STANDARD',
|
|
730
|
+
' 50', '0.0',
|
|
731
|
+
' 72', '0',
|
|
732
|
+
' 11', formatNumber(position.x),
|
|
733
|
+
' 21', formatNumber(position.y),
|
|
734
|
+
' 31', '0.0'
|
|
735
|
+
];
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Create DXF RECTANGLE (as LWPOLYLINE)
|
|
740
|
+
* @private
|
|
741
|
+
*/
|
|
742
|
+
function createDXFRectangle(points, layer = '0') {
|
|
743
|
+
const p1 = points[0];
|
|
744
|
+
const p2 = points[1];
|
|
745
|
+
|
|
746
|
+
const corners = [
|
|
747
|
+
{ x: p1.x, y: p1.y },
|
|
748
|
+
{ x: p2.x, y: p1.y },
|
|
749
|
+
{ x: p2.x, y: p2.y },
|
|
750
|
+
{ x: p1.x, y: p2.y },
|
|
751
|
+
{ x: p1.x, y: p1.y } // Close loop
|
|
752
|
+
];
|
|
753
|
+
|
|
754
|
+
return createDXFPolyline(corners, layer);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Create DXF 3DFACE entity
|
|
759
|
+
* @private
|
|
760
|
+
*/
|
|
761
|
+
function createDXF3DFace(faceVertices, layer = '0') {
|
|
762
|
+
const lines = [
|
|
763
|
+
' 0', '3DFACE',
|
|
764
|
+
' 8', layer
|
|
765
|
+
];
|
|
766
|
+
|
|
767
|
+
// Support triangles and quads
|
|
768
|
+
const vCount = Math.min(faceVertices.length, 4);
|
|
769
|
+
for (let i = 0; i < vCount; i++) {
|
|
770
|
+
const v = faceVertices[i];
|
|
771
|
+
lines.push(` 1${i}`);
|
|
772
|
+
lines.push(formatNumber(v.x));
|
|
773
|
+
lines.push(` 2${i}`);
|
|
774
|
+
lines.push(formatNumber(v.y));
|
|
775
|
+
lines.push(` 3${i}`);
|
|
776
|
+
lines.push(formatNumber(v.z));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return lines;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Create DXF 3DLINE entity
|
|
784
|
+
* @private
|
|
785
|
+
*/
|
|
786
|
+
function createDXF3DLine(p1, p2, layer = '0') {
|
|
787
|
+
return [
|
|
788
|
+
' 0', 'LINE',
|
|
789
|
+
' 8', layer,
|
|
790
|
+
' 10', formatNumber(p1.x),
|
|
791
|
+
' 20', formatNumber(p1.y),
|
|
792
|
+
' 30', formatNumber(p1.z),
|
|
793
|
+
' 11', formatNumber(p2.x),
|
|
794
|
+
' 21', formatNumber(p2.y),
|
|
795
|
+
' 31', formatNumber(p2.z)
|
|
796
|
+
];
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ============================================================================
|
|
800
|
+
// Geometry Processing
|
|
801
|
+
// ============================================================================
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Extract visible projected edges from mesh
|
|
805
|
+
* @private
|
|
806
|
+
*/
|
|
807
|
+
function extractProjectedEdges(mesh, projMatrix, scale = 1.0, isVisible = true) {
|
|
808
|
+
const edges = [];
|
|
809
|
+
const frustum = new THREE.Frustum();
|
|
810
|
+
const cameraMatrix = new THREE.Matrix4();
|
|
811
|
+
cameraMatrix.multiplyMatrices(projMatrix, mesh.matrixWorld);
|
|
812
|
+
frustum.setFromProjectionMatrix(cameraMatrix);
|
|
813
|
+
|
|
814
|
+
const tempMesh = mesh instanceof THREE.Group ? mesh : mesh;
|
|
815
|
+
|
|
816
|
+
tempMesh.traverse(child => {
|
|
817
|
+
if (child instanceof THREE.Mesh && child.geometry) {
|
|
818
|
+
const geometry = child.geometry;
|
|
819
|
+
|
|
820
|
+
if (!geometry.attributes.position) return;
|
|
821
|
+
|
|
822
|
+
const positions = geometry.attributes.position.array;
|
|
823
|
+
const indices = geometry.index ? geometry.index.array : null;
|
|
824
|
+
|
|
825
|
+
// Extract edges from geometry
|
|
826
|
+
const extractedEdges = indices ?
|
|
827
|
+
extractEdgesFromIndices(positions, indices) :
|
|
828
|
+
extractEdgesFromVertices(positions);
|
|
829
|
+
|
|
830
|
+
extractedEdges.forEach(edge => {
|
|
831
|
+
const v1 = new THREE.Vector3(edge.start.x * scale, edge.start.y * scale, 0);
|
|
832
|
+
const v2 = new THREE.Vector3(edge.end.x * scale, edge.end.y * scale, 0);
|
|
833
|
+
edges.push({ start: v1, end: v2 });
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
return edges;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Extract edges from indexed geometry
|
|
843
|
+
* @private
|
|
844
|
+
*/
|
|
845
|
+
function extractEdgesFromIndices(positions, indices) {
|
|
846
|
+
const edges = [];
|
|
847
|
+
const edgeSet = new Set();
|
|
848
|
+
|
|
849
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
850
|
+
const i0 = indices[i] * 3;
|
|
851
|
+
const i1 = indices[i + 1] * 3;
|
|
852
|
+
const i2 = indices[i + 2] * 3;
|
|
853
|
+
|
|
854
|
+
const p0 = { x: positions[i0], y: positions[i0 + 1] };
|
|
855
|
+
const p1 = { x: positions[i1], y: positions[i1 + 1] };
|
|
856
|
+
const p2 = { x: positions[i2], y: positions[i2 + 1] };
|
|
857
|
+
|
|
858
|
+
addEdgeToSet(edgeSet, p0, p1, edges);
|
|
859
|
+
addEdgeToSet(edgeSet, p1, p2, edges);
|
|
860
|
+
addEdgeToSet(edgeSet, p2, p0, edges);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return edges;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Extract edges from non-indexed geometry
|
|
868
|
+
* @private
|
|
869
|
+
*/
|
|
870
|
+
function extractEdgesFromVertices(positions) {
|
|
871
|
+
const edges = [];
|
|
872
|
+
|
|
873
|
+
for (let i = 0; i < positions.length; i += 9) {
|
|
874
|
+
const p0 = { x: positions[i], y: positions[i + 1] };
|
|
875
|
+
const p1 = { x: positions[i + 3], y: positions[i + 4] };
|
|
876
|
+
const p2 = { x: positions[i + 6], y: positions[i + 7] };
|
|
877
|
+
|
|
878
|
+
edges.push({ start: p0, end: p1 });
|
|
879
|
+
edges.push({ start: p1, end: p2 });
|
|
880
|
+
edges.push({ start: p2, end: p0 });
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return edges;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Add edge to set, avoiding duplicates
|
|
888
|
+
* @private
|
|
889
|
+
*/
|
|
890
|
+
function addEdgeToSet(edgeSet, p1, p2, edges) {
|
|
891
|
+
const key = [
|
|
892
|
+
Math.round(p1.x * 1000),
|
|
893
|
+
Math.round(p1.y * 1000),
|
|
894
|
+
Math.round(p2.x * 1000),
|
|
895
|
+
Math.round(p2.y * 1000)
|
|
896
|
+
].join(',');
|
|
897
|
+
|
|
898
|
+
if (!edgeSet.has(key)) {
|
|
899
|
+
edgeSet.add(key);
|
|
900
|
+
edges.push({ start: p1, end: p2 });
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Extract geometry from mesh/group
|
|
906
|
+
* @private
|
|
907
|
+
*/
|
|
908
|
+
function extractGeometry(mesh, vertices, faces) {
|
|
909
|
+
let vertexOffset = vertices.length;
|
|
910
|
+
|
|
911
|
+
mesh.traverse(child => {
|
|
912
|
+
if (child instanceof THREE.Mesh && child.geometry) {
|
|
913
|
+
const geometry = child.geometry;
|
|
914
|
+
|
|
915
|
+
if (!geometry.attributes.position) return;
|
|
916
|
+
|
|
917
|
+
const positions = geometry.attributes.position.array;
|
|
918
|
+
|
|
919
|
+
// Add vertices
|
|
920
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
921
|
+
vertices.push({
|
|
922
|
+
x: positions[i],
|
|
923
|
+
y: positions[i + 1],
|
|
924
|
+
z: positions[i + 2]
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Add faces
|
|
929
|
+
const indices = geometry.index ? geometry.index.array : null;
|
|
930
|
+
|
|
931
|
+
if (indices) {
|
|
932
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
933
|
+
faces.push([
|
|
934
|
+
vertexOffset + indices[i],
|
|
935
|
+
vertexOffset + indices[i + 1],
|
|
936
|
+
vertexOffset + indices[i + 2]
|
|
937
|
+
]);
|
|
938
|
+
}
|
|
939
|
+
} else {
|
|
940
|
+
for (let i = 0; i < positions.length / 3; i += 3) {
|
|
941
|
+
faces.push([
|
|
942
|
+
vertexOffset + i,
|
|
943
|
+
vertexOffset + i + 1,
|
|
944
|
+
vertexOffset + i + 2
|
|
945
|
+
]);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
vertexOffset = vertices.length;
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Extract unique edges from faces
|
|
956
|
+
* @private
|
|
957
|
+
*/
|
|
958
|
+
function extractEdges(faces) {
|
|
959
|
+
const edgeSet = new Set();
|
|
960
|
+
const edges = [];
|
|
961
|
+
|
|
962
|
+
faces.forEach(face => {
|
|
963
|
+
for (let i = 0; i < face.length; i++) {
|
|
964
|
+
const v1 = face[i];
|
|
965
|
+
const v2 = face[(i + 1) % face.length];
|
|
966
|
+
|
|
967
|
+
const key = [Math.min(v1, v2), Math.max(v1, v2)].join(',');
|
|
968
|
+
|
|
969
|
+
if (!edgeSet.has(key)) {
|
|
970
|
+
edgeSet.add(key);
|
|
971
|
+
edges.push({ v1, v2 });
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
return edges;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Get projection matrix for standard view
|
|
981
|
+
* @private
|
|
982
|
+
*/
|
|
983
|
+
function getProjectionMatrix(view) {
|
|
984
|
+
const matrix = new THREE.Matrix4();
|
|
985
|
+
|
|
986
|
+
switch (view.toLowerCase()) {
|
|
987
|
+
case 'front':
|
|
988
|
+
// Looking at Z=0 plane from positive Z
|
|
989
|
+
matrix.set(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
|
990
|
+
break;
|
|
991
|
+
case 'top':
|
|
992
|
+
// Looking at XY plane from positive Z (top-down)
|
|
993
|
+
matrix.set(1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1);
|
|
994
|
+
break;
|
|
995
|
+
case 'right':
|
|
996
|
+
// Looking at YZ plane from positive X (right side)
|
|
997
|
+
matrix.set(0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1);
|
|
998
|
+
break;
|
|
999
|
+
case 'back':
|
|
1000
|
+
// Looking at Z=0 plane from negative Z
|
|
1001
|
+
matrix.set(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1);
|
|
1002
|
+
break;
|
|
1003
|
+
case 'left':
|
|
1004
|
+
// Looking at YZ plane from negative X
|
|
1005
|
+
matrix.set(0, 0, -1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 1);
|
|
1006
|
+
break;
|
|
1007
|
+
case 'bottom':
|
|
1008
|
+
// Looking at XY plane from negative Z
|
|
1009
|
+
matrix.set(1, 0, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, 0, 1);
|
|
1010
|
+
break;
|
|
1011
|
+
case 'iso':
|
|
1012
|
+
// Isometric view
|
|
1013
|
+
matrix.set(0.866, -0.5, 0, 0, 0.433, 0.75, -0.5, 0, 0.5, 0.433, 0.75, 0, 0, 0, 0, 1);
|
|
1014
|
+
break;
|
|
1015
|
+
default:
|
|
1016
|
+
matrix.identity();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
return matrix;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// ============================================================================
|
|
1023
|
+
// Utility Functions
|
|
1024
|
+
// ============================================================================
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Calculate extents from 2D sketch entities
|
|
1028
|
+
* @private
|
|
1029
|
+
*/
|
|
1030
|
+
function calculateExtents(entities) {
|
|
1031
|
+
let minX = 0, minY = 0, maxX = 100, maxY = 100;
|
|
1032
|
+
|
|
1033
|
+
entities.forEach(entity => {
|
|
1034
|
+
entity.points?.forEach(p => {
|
|
1035
|
+
minX = Math.min(minX, p.x);
|
|
1036
|
+
minY = Math.min(minY, p.y);
|
|
1037
|
+
maxX = Math.max(maxX, p.x);
|
|
1038
|
+
maxY = Math.max(maxY, p.y);
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
return {
|
|
1043
|
+
min: { x: minX - 10, y: minY - 10 },
|
|
1044
|
+
max: { x: maxX + 10, y: maxY + 10 }
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Calculate extents from projected edges
|
|
1050
|
+
* @private
|
|
1051
|
+
*/
|
|
1052
|
+
function calculateExtentsFromEdges(edges) {
|
|
1053
|
+
let minX = 0, minY = 0, maxX = 100, maxY = 100;
|
|
1054
|
+
|
|
1055
|
+
if (edges.length === 0) {
|
|
1056
|
+
return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY } };
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
minX = edges[0].start.x;
|
|
1060
|
+
minY = edges[0].start.y;
|
|
1061
|
+
maxX = edges[0].start.x;
|
|
1062
|
+
maxY = edges[0].start.y;
|
|
1063
|
+
|
|
1064
|
+
edges.forEach(edge => {
|
|
1065
|
+
minX = Math.min(minX, edge.start.x, edge.end.x);
|
|
1066
|
+
minY = Math.min(minY, edge.start.y, edge.end.y);
|
|
1067
|
+
maxX = Math.max(maxX, edge.start.x, edge.end.x);
|
|
1068
|
+
maxY = Math.max(maxY, edge.start.y, edge.end.y);
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
const padding = 10;
|
|
1072
|
+
return {
|
|
1073
|
+
min: { x: minX - padding, y: minY - padding },
|
|
1074
|
+
max: { x: maxX + padding, y: maxY + padding }
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Calculate midpoint of points
|
|
1080
|
+
* @private
|
|
1081
|
+
*/
|
|
1082
|
+
function calculateMidpoint(points) {
|
|
1083
|
+
if (!points || points.length === 0) return { x: 0, y: 0 };
|
|
1084
|
+
|
|
1085
|
+
const sum = points.reduce((acc, p) => ({
|
|
1086
|
+
x: acc.x + p.x,
|
|
1087
|
+
y: acc.y + p.y
|
|
1088
|
+
}), { x: 0, y: 0 });
|
|
1089
|
+
|
|
1090
|
+
return {
|
|
1091
|
+
x: sum.x / points.length,
|
|
1092
|
+
y: sum.y / points.length
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Format number for DXF (6 decimal places)
|
|
1098
|
+
* @private
|
|
1099
|
+
*/
|
|
1100
|
+
function formatNumber(num) {
|
|
1101
|
+
return Number(num).toFixed(6);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// ============================================================================
|
|
1105
|
+
// File Download & Export
|
|
1106
|
+
// ============================================================================
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Trigger browser download of DXF file
|
|
1110
|
+
*
|
|
1111
|
+
* @param {string} content - DXF file content
|
|
1112
|
+
* @param {string} filename - Output filename (default 'export.dxf')
|
|
1113
|
+
*/
|
|
1114
|
+
export function downloadDXF(content, filename = 'export.dxf') {
|
|
1115
|
+
const blob = new Blob([content], { type: 'application/vnd.dxf' });
|
|
1116
|
+
const url = URL.createObjectURL(blob);
|
|
1117
|
+
const link = document.createElement('a');
|
|
1118
|
+
link.href = url;
|
|
1119
|
+
link.download = filename.endsWith('.dxf') ? filename : `${filename}.dxf`;
|
|
1120
|
+
document.body.appendChild(link);
|
|
1121
|
+
link.click();
|
|
1122
|
+
document.body.removeChild(link);
|
|
1123
|
+
URL.revokeObjectURL(url);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Convert DXF sections to string
|
|
1128
|
+
* Utility for building custom DXF files
|
|
1129
|
+
*
|
|
1130
|
+
* @param {Object} sections - Object with section names as keys: { header: '...', tables: '...', ... }
|
|
1131
|
+
* @returns {string} Complete DXF content
|
|
1132
|
+
*/
|
|
1133
|
+
export function dxfToString(sections) {
|
|
1134
|
+
return Object.values(sections).join('\n');
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// ============================================================================
|
|
1138
|
+
// Export Summary
|
|
1139
|
+
// ============================================================================
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Generate a text summary of what will be exported
|
|
1143
|
+
* Useful for UI feedback before export
|
|
1144
|
+
*
|
|
1145
|
+
* @param {Array|THREE.Mesh} data - Entities array or mesh
|
|
1146
|
+
* @param {string} type - Export type: 'sketch'|'projection'|'multiview'|'3d'
|
|
1147
|
+
* @returns {string} Human-readable summary
|
|
1148
|
+
*/
|
|
1149
|
+
export function generateExportSummary(data, type = 'sketch') {
|
|
1150
|
+
let summary = '';
|
|
1151
|
+
|
|
1152
|
+
switch (type) {
|
|
1153
|
+
case 'sketch':
|
|
1154
|
+
summary = `Exporting ${data.length} sketch entities`;
|
|
1155
|
+
const lines = data.filter(e => e.type === 'line').length;
|
|
1156
|
+
const circles = data.filter(e => e.type === 'circle').length;
|
|
1157
|
+
if (lines > 0) summary += `\n- ${lines} lines`;
|
|
1158
|
+
if (circles > 0) summary += `\n- ${circles} circles`;
|
|
1159
|
+
break;
|
|
1160
|
+
case 'projection':
|
|
1161
|
+
summary = 'Exporting 2D projection view';
|
|
1162
|
+
break;
|
|
1163
|
+
case 'multiview':
|
|
1164
|
+
summary = 'Exporting multi-view engineering drawing\n- Front, Top, Right views\n- Title block included';
|
|
1165
|
+
break;
|
|
1166
|
+
case '3d':
|
|
1167
|
+
summary = 'Exporting 3D wireframe';
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
summary += '\n\nFormat: AutoCAD R14 (ASCII DXF)\nUnits: millimeters';
|
|
1172
|
+
return summary;
|
|
1173
|
+
}
|