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.
@@ -0,0 +1,1138 @@
1
+ /**
2
+ * Inventor File Parser โ€” ES Module (v2)
3
+ * Parses .ipt (part) and .iam (assembly) files directly in the browser
4
+ * Zero dependencies, pure OLE2/CFB container parsing + multi-stream analysis
5
+ * Generates Fusion 360 + cycleCAD reconstruction guides
6
+ */
7
+
8
+ // ============================================================================
9
+ // CONSTANTS & HELPERS
10
+ // ============================================================================
11
+
12
+ const OLE2_SIGNATURE = 0xD0CF11E0;
13
+ const SECTOR_SIZE = 512;
14
+ const MINI_SECTOR_SIZE = 64;
15
+ const DIR_ENTRY_SIZE = 128;
16
+ const FAT_SECTOR_ID = 0xFFFFFFFD;
17
+ const ENDOFCHAIN = 0xFFFFFFFE;
18
+ const FREECHAIN = 0xFFFFFFFF;
19
+
20
+ // Feature type patterns (ASCII strings to search in binary)
21
+ const FEATURE_PATTERNS = [
22
+ { pattern: 'ExtrudeFeature', type: 'Extrude', icon: '๐Ÿ“ฆ', color: '#3fb950', f360: 'Extrude', cycleCAD: 'extrudeProfile' },
23
+ { pattern: 'RevolveFeature', type: 'Revolve', icon: '๐Ÿ”„', color: '#d29922', f360: 'Revolve', cycleCAD: 'revolveProfile' },
24
+ { pattern: 'HoleFeature', type: 'Hole', icon: 'โญ•', color: '#f85149', f360: 'Hole', cycleCAD: 'createHole' },
25
+ { pattern: 'FilletFeature', type: 'Fillet', icon: 'โŒ’', color: '#a371f7', f360: 'Fillet', cycleCAD: 'applyFillet' },
26
+ { pattern: 'ChamferFeature', type: 'Chamfer', icon: 'โŒ‰', color: '#db61a2', f360: 'Chamfer', cycleCAD: 'applyChamfer' },
27
+ { pattern: 'Sketch2D', type: 'Sketch', icon: 'โœ๏ธ', color: '#58a6ff', f360: 'Create Sketch', cycleCAD: 'startSketch' },
28
+ { pattern: 'Sketch3D', type: '3D Sketch', icon: 'โœ๏ธ', color: '#58a6ff', f360: 'Create 3D Sketch', cycleCAD: 'startSketch3D' },
29
+ { pattern: 'RectangularPatternFeature', type: 'RectPattern', icon: 'โ–', color: '#58a6ff', f360: 'Rectangular Pattern', cycleCAD: 'rectPattern' },
30
+ { pattern: 'CircularPatternFeature', type: 'CircPattern', icon: 'โ–', color: '#58a6ff', f360: 'Circular Pattern', cycleCAD: 'circPattern' },
31
+ { pattern: 'MirrorFeature', type: 'Mirror', icon: 'โ‡„', color: '#79c0ff', f360: 'Mirror', cycleCAD: 'mirror' },
32
+ { pattern: 'SweepFeature', type: 'Sweep', icon: '๐ŸŒ€', color: '#a371f7', f360: 'Sweep', cycleCAD: 'sweepProfile' },
33
+ { pattern: 'LoftFeature', type: 'Loft', icon: '๐ŸŒŠ', color: '#3fb950', f360: 'Loft', cycleCAD: 'loftProfile' },
34
+ { pattern: 'ShellFeature', type: 'Shell', icon: '๐Ÿš', color: '#d29922', f360: 'Shell', cycleCAD: 'shellBody' },
35
+ { pattern: 'ThreadFeature', type: 'Thread', icon: '๐Ÿ”ฉ', color: '#8b949e', f360: 'Thread', cycleCAD: 'addThread' },
36
+ { pattern: 'WorkPlane', type: 'WorkPlane', icon: '๐Ÿ“', color: '#8b949e', f360: 'Construction Plane', cycleCAD: 'createWorkPlane' },
37
+ { pattern: 'WorkAxis', type: 'WorkAxis', icon: '๐Ÿ“', color: '#8b949e', f360: 'Construction Axis', cycleCAD: 'createWorkAxis' },
38
+ { pattern: 'WorkPoint', type: 'WorkPoint', icon: 'โ€ข', color: '#8b949e', f360: 'Construction Point', cycleCAD: 'createWorkPoint' },
39
+ { pattern: 'BooleanFeature', type: 'Boolean', icon: 'โœ‚๏ธ', color: '#f85149', f360: 'Combine', cycleCAD: 'booleanOp' },
40
+ { pattern: 'CutFeature', type: 'Cut', icon: 'โœ‚๏ธ', color: '#f85149', f360: 'Extrude (Cut)', cycleCAD: 'cutExtrude' },
41
+ { pattern: 'SplitFeature', type: 'Split', icon: 'โœ‚๏ธ', color: '#f85149', f360: 'Split Body', cycleCAD: 'splitBody' },
42
+ // Sheet metal features
43
+ { pattern: 'FlangeFeature', type: 'Flange', icon: '๐Ÿ“„', color: '#d29922', f360: 'Flange', cycleCAD: 'addFlange' },
44
+ { pattern: 'BendFeature', type: 'Bend', icon: 'โ†ช', color: '#d29922', f360: 'Bend', cycleCAD: 'addBend' },
45
+ { pattern: 'HemFeature', type: 'Hem', icon: 'โคต', color: '#d29922', f360: 'Hem', cycleCAD: 'addHem' },
46
+ { pattern: 'FoldFeature', type: 'Fold', icon: '๐Ÿ“', color: '#d29922', f360: 'Fold', cycleCAD: 'addFold' },
47
+ { pattern: 'FlatPattern', type: 'FlatPattern', icon: '๐Ÿ“‹', color: '#3fb950', f360: 'Create Flat Pattern', cycleCAD: 'createFlatPattern' },
48
+ { pattern: 'ContourFlangeFeature', type: 'ContourFlange', icon: '๐Ÿ“„', color: '#d29922', f360: 'Contour Flange', cycleCAD: 'addContourFlange' },
49
+ { pattern: 'FaceFeature', type: 'Face', icon: 'โ–ข', color: '#3fb950', f360: 'Face', cycleCAD: 'createFace' },
50
+ { pattern: 'UnfoldFeature', type: 'Unfold', icon: 'โ†—', color: '#d29922', f360: 'Unfold', cycleCAD: 'unfold' },
51
+ ];
52
+
53
+ // Constraint type patterns for assemblies
54
+ const CONSTRAINT_PATTERNS = [
55
+ { pattern: 'MateConstraint', f360: 'Joint > Rigid', type: 'Mate' },
56
+ { pattern: 'FlushConstraint', f360: 'Joint > Planar', type: 'Flush' },
57
+ { pattern: 'AngleConstraint', f360: 'Joint > Revolute', type: 'Angle' },
58
+ { pattern: 'InsertConstraint', f360: 'Joint > Cylindrical', type: 'Insert' },
59
+ { pattern: 'TangentConstraint', f360: 'Joint > Slider', type: 'Tangent' },
60
+ { pattern: 'CoincidentConstraint', f360: 'Joint > Rigid', type: 'Coincident' },
61
+ { pattern: 'ParallelConstraint', f360: 'Joint > Rigid', type: 'Parallel' },
62
+ { pattern: 'PerpendicularConstraint', f360: 'Joint > Rigid', type: 'Perpendicular' },
63
+ { pattern: 'ConcentricConstraint', f360: 'Joint > Revolute', type: 'Concentric' },
64
+ ];
65
+
66
+ // Inventor template โ†’ part type mapping
67
+ const TEMPLATE_MAP = {
68
+ 'SheetMetal': { type: 'Sheet Metal', f360Tab: 'SHEET METAL', icon: '๐Ÿ“„' },
69
+ 'Standard': { type: 'Solid Part', f360Tab: 'SOLID', icon: '๐Ÿ“ฆ' },
70
+ 'Weldment': { type: 'Weldment', f360Tab: 'SOLID', icon: '๐Ÿ”ง' },
71
+ 'Mold': { type: 'Mold Design', f360Tab: 'SOLID', icon: '๐Ÿญ' },
72
+ };
73
+
74
+ // ============================================================================
75
+ // OLE2/CFB PARSER
76
+ // ============================================================================
77
+
78
+ function parseOLE2(buffer) {
79
+ const view = new DataView(buffer.buffer || buffer);
80
+ const bufArr = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
81
+
82
+ // Validate signature
83
+ const sig1 = view.getUint32(0, false);
84
+ if (sig1 !== 0xD0CF11E0) {
85
+ throw new Error('Invalid OLE2 signature โ€” not an Inventor file');
86
+ }
87
+
88
+ const sectorSizePow = view.getUint16(30, true);
89
+ const sectorSize = 1 << sectorSizePow;
90
+ const totalFATSectors = view.getUint32(44, true);
91
+ const dirStartSector = view.getInt32(48, true);
92
+ const version = `${view.getUint16(26, true)}.${view.getUint16(24, true)}`;
93
+
94
+ // Build FAT from header DIFAT entries
95
+ const fat = [];
96
+ for (let i = 0; i < Math.min(totalFATSectors, 109); i++) {
97
+ const fatSectorID = view.getInt32(76 + i * 4, true);
98
+ if (fatSectorID < 0) break;
99
+ const offset = (fatSectorID + 1) * sectorSize;
100
+ if (offset + sectorSize > bufArr.length) break;
101
+ for (let j = 0; j < sectorSize / 4; j++) {
102
+ fat.push(view.getInt32(offset + j * 4, true));
103
+ }
104
+ }
105
+
106
+ // Read sector chain
107
+ function readChain(startSector, maxSize = Infinity) {
108
+ const chunks = [];
109
+ let sector = startSector;
110
+ let totalRead = 0;
111
+ let safety = 0;
112
+ while (sector >= 0 && sector < fat.length && safety < 50000 && totalRead < maxSize) {
113
+ const offset = (sector + 1) * sectorSize;
114
+ if (offset + sectorSize > bufArr.length) break;
115
+ chunks.push(bufArr.slice(offset, offset + sectorSize));
116
+ totalRead += sectorSize;
117
+ sector = fat[sector];
118
+ safety++;
119
+ }
120
+ const result = new Uint8Array(totalRead);
121
+ let pos = 0;
122
+ for (const c of chunks) { result.set(c, pos); pos += c.length; }
123
+ return result;
124
+ }
125
+
126
+ // Read directory entries
127
+ const dirData = readChain(dirStartSector, 200 * sectorSize);
128
+ const entries = [];
129
+ for (let i = 0; i < dirData.length; i += 128) {
130
+ const nameLen = dirData[i + 64] | (dirData[i + 65] << 8);
131
+ let name = '';
132
+ for (let c = 0; c < Math.min(nameLen, 64) - 2; c += 2) {
133
+ const ch = dirData[i + c] | (dirData[i + c + 1] << 8);
134
+ if (ch === 0) break;
135
+ name += String.fromCharCode(ch);
136
+ }
137
+ const type = dirData[i + 66];
138
+ const startSector = dirData[i + 116] | (dirData[i + 117] << 8) | (dirData[i + 118] << 16) | (dirData[i + 119] << 24);
139
+ const size = dirData[i + 120] | (dirData[i + 121] << 8) | (dirData[i + 122] << 16) | (dirData[i + 123] << 24);
140
+ if (type > 0 && name) entries.push({ name, type, startSector, size });
141
+ }
142
+
143
+ return {
144
+ version,
145
+ sectorSize,
146
+ entries,
147
+ fat,
148
+ getStream(name) {
149
+ const entry = entries.find(e => e.name === name && e.type === 2);
150
+ if (!entry || entry.size <= 0) return null;
151
+ const raw = readChain(entry.startSector, entry.size + sectorSize);
152
+ return raw.slice(0, Math.min(raw.length, entry.size));
153
+ },
154
+ getAllStreams() {
155
+ return entries.filter(e => e.type === 2 && e.size > 0);
156
+ }
157
+ };
158
+ }
159
+
160
+ // ============================================================================
161
+ // UFRxDoc PARSER โ€” This is where the REAL feature tree lives!
162
+ // ============================================================================
163
+
164
+ function parseUFRxDoc(ole2) {
165
+ const data = ole2.getStream('UFRxDoc');
166
+ if (!data) return null;
167
+
168
+ const result = {
169
+ template: '',
170
+ partType: 'Standard',
171
+ originalPath: '',
172
+ savedFrom: '',
173
+ savedOn: '',
174
+ fileSchema: '',
175
+ softwareSchema: '',
176
+ features: [],
177
+ parameters: [],
178
+ references: [],
179
+ bodies: [],
180
+ exportStates: [],
181
+ };
182
+
183
+ // Decode as UTF-16LE (Inventor uses Windows UTF-16)
184
+ const utf16 = new TextDecoder('utf-16le', { fatal: false }).decode(data);
185
+
186
+ // Also get ASCII view for binary searches
187
+ const ascii = new TextDecoder('ascii', { fatal: false }).decode(data);
188
+
189
+ // Extract template type
190
+ const templateMatch = utf16.match(/Templates\\[^\\]*\\([^.]+)\.ipt/i) ||
191
+ utf16.match(/Templates\\[^\\]*\\([^.]+)\.iam/i);
192
+ if (templateMatch) {
193
+ result.template = templateMatch[1];
194
+ // Detect part type from template
195
+ for (const [key, val] of Object.entries(TEMPLATE_MAP)) {
196
+ if (result.template.toLowerCase().includes(key.toLowerCase())) {
197
+ result.partType = val.type;
198
+ break;
199
+ }
200
+ }
201
+ }
202
+
203
+ // Extract version info
204
+ const schemaMatch = utf16.match(/FileSchema:\s*([\d.]+)/);
205
+ if (schemaMatch) result.fileSchema = schemaMatch[1];
206
+
207
+ const softwareMatch = utf16.match(/SoftwareSchema:\s*([\d.]+)/);
208
+ if (softwareMatch) result.softwareSchema = softwareMatch[1];
209
+
210
+ const savedFromMatch = utf16.match(/SavedFrom:\s*([^\0]+)/);
211
+ if (savedFromMatch) result.savedFrom = savedFromMatch[1].trim();
212
+
213
+ const savedOnMatch = utf16.match(/SavedOn:\s*([^\0]+)/);
214
+ if (savedOnMatch) result.savedOn = savedOnMatch[1].trim();
215
+
216
+ // Extract original file path
217
+ const pathMatch = utf16.match(/([A-Z]:\\[^\0]{10,}\.ipt)/i) ||
218
+ utf16.match(/([A-Z]:\\[^\0]{10,}\.iam)/i);
219
+ if (pathMatch) result.originalPath = pathMatch[1];
220
+
221
+ // Extract Body references
222
+ const bodyRegex = /Body\s*\(\s*Body\s*\)/g;
223
+ let bodyMatch;
224
+ while ((bodyMatch = utf16.matchAll(/Body\s*\(([^)]*)\)/g))) {
225
+ for (const m of bodyMatch) {
226
+ result.bodies.push(m[1] || 'Body');
227
+ }
228
+ break;
229
+ }
230
+
231
+ // Extract Export/Feature states from UFRxDoc
232
+ const exportRegex = /Export([A-Za-z]+)/g;
233
+ let exportMatch2;
234
+ while ((exportMatch2 = exportRegex.exec(utf16)) !== null) {
235
+ const name = exportMatch2[1];
236
+ if (name !== 'GlobalState' && !result.exportStates.includes(name)) {
237
+ result.exportStates.push(name);
238
+ }
239
+ }
240
+
241
+ // Extract parameter references from UFRxDoc
242
+ const paramRegex = /([A-Za-z_]\w*)\s*\(\s*Parameter\s*\)\s*(\d+)/g;
243
+ let paramMatch2;
244
+ while ((paramMatch2 = paramRegex.exec(utf16)) !== null) {
245
+ result.parameters.push({
246
+ name: paramMatch2[1],
247
+ id: parseInt(paramMatch2[2]),
248
+ source: 'UFRxDoc'
249
+ });
250
+ }
251
+
252
+ // Extract file references (for assemblies or linked parts)
253
+ const refRegex = /([A-Za-z0-9_\- ]+\.ipt)/gi;
254
+ let refMatch;
255
+ while ((refMatch = refRegex.exec(utf16)) !== null) {
256
+ const ref = refMatch[1].trim();
257
+ if (ref.length > 4 && !result.references.includes(ref)) {
258
+ result.references.push(ref);
259
+ }
260
+ }
261
+
262
+ // Scan for feature keywords in both UFRxDoc and ascii view
263
+ for (const fp of FEATURE_PATTERNS) {
264
+ // Check UTF-16
265
+ if (utf16.includes(fp.pattern)) {
266
+ const existing = result.features.find(f => f.type === fp.type);
267
+ if (!existing) {
268
+ result.features.push({
269
+ type: fp.type,
270
+ pattern: fp.pattern,
271
+ icon: fp.icon,
272
+ color: fp.color,
273
+ f360: fp.f360,
274
+ cycleCAD: fp.cycleCAD,
275
+ source: 'UFRxDoc',
276
+ count: (utf16.match(new RegExp(fp.pattern, 'g')) || []).length
277
+ });
278
+ }
279
+ }
280
+ // Check ASCII
281
+ if (ascii.includes(fp.pattern)) {
282
+ const existing = result.features.find(f => f.type === fp.type);
283
+ if (!existing) {
284
+ result.features.push({
285
+ type: fp.type,
286
+ pattern: fp.pattern,
287
+ icon: fp.icon,
288
+ color: fp.color,
289
+ f360: fp.f360,
290
+ cycleCAD: fp.cycleCAD,
291
+ source: 'UFRxDoc-ascii',
292
+ count: (ascii.match(new RegExp(fp.pattern, 'g')) || []).length
293
+ });
294
+ }
295
+ }
296
+ }
297
+
298
+ return result;
299
+ }
300
+
301
+ // ============================================================================
302
+ // MULTI-STREAM FEATURE SCANNER
303
+ // ============================================================================
304
+
305
+ function scanAllStreams(ole2) {
306
+ const allFeatures = [];
307
+ const allParams = [];
308
+ const fileRefs = [];
309
+ const constraints = [];
310
+
311
+ const streams = ole2.getAllStreams();
312
+
313
+ for (const entry of streams) {
314
+ if (entry.size < 50) continue;
315
+
316
+ try {
317
+ const data = ole2.getStream(entry.name);
318
+ if (!data) continue;
319
+
320
+ // ASCII decode
321
+ const ascii = new TextDecoder('ascii', { fatal: false }).decode(data);
322
+ // UTF-16 decode for wider coverage
323
+ const utf16 = new TextDecoder('utf-16le', { fatal: false }).decode(data);
324
+
325
+ // Scan for features
326
+ for (const fp of FEATURE_PATTERNS) {
327
+ const asciiMatches = (ascii.match(new RegExp(fp.pattern, 'g')) || []).length;
328
+ const utf16Matches = (utf16.match(new RegExp(fp.pattern, 'g')) || []).length;
329
+ const count = Math.max(asciiMatches, utf16Matches);
330
+ if (count > 0) {
331
+ allFeatures.push({
332
+ type: fp.type,
333
+ pattern: fp.pattern,
334
+ icon: fp.icon,
335
+ color: fp.color,
336
+ f360: fp.f360,
337
+ cycleCAD: fp.cycleCAD,
338
+ source: entry.name,
339
+ count
340
+ });
341
+ }
342
+ }
343
+
344
+ // Scan for .ipt/.iam references
345
+ const iptRefs = [...utf16.matchAll(/([A-Za-z0-9_\- ]+\.ipt)/gi)];
346
+ const iamRefs = [...utf16.matchAll(/([A-Za-z0-9_\- ]+\.iam)/gi)];
347
+ for (const ref of [...iptRefs, ...iamRefs]) {
348
+ const name = ref[1].trim();
349
+ if (name.length > 4 && !fileRefs.includes(name)) fileRefs.push(name);
350
+ }
351
+
352
+ // Scan for constraints
353
+ for (const cp of CONSTRAINT_PATTERNS) {
354
+ if (ascii.includes(cp.pattern) || utf16.includes(cp.pattern)) {
355
+ if (!constraints.find(c => c.type === cp.type)) {
356
+ constraints.push({ type: cp.type, f360: cp.f360, source: entry.name });
357
+ }
358
+ }
359
+ }
360
+
361
+ } catch (e) {
362
+ // Skip unreadable streams
363
+ }
364
+ }
365
+
366
+ // Deduplicate features by type, keep highest count
367
+ const featureMap = new Map();
368
+ for (const f of allFeatures) {
369
+ const existing = featureMap.get(f.type);
370
+ if (!existing || f.count > existing.count) {
371
+ featureMap.set(f.type, f);
372
+ }
373
+ }
374
+
375
+ return {
376
+ features: Array.from(featureMap.values()),
377
+ fileRefs,
378
+ constraints
379
+ };
380
+ }
381
+
382
+ // ============================================================================
383
+ // FUSION 360 RECONSTRUCTION GUIDE GENERATOR
384
+ // ============================================================================
385
+
386
+ function generateFusion360Guide(parsedData) {
387
+ const steps = [];
388
+ let stepNum = 1;
389
+
390
+ // Step 1: Create new document
391
+ const partType = parsedData.ufrxDoc?.partType || 'Standard';
392
+ const isSheetMetal = partType === 'Sheet Metal';
393
+
394
+ steps.push({
395
+ step: stepNum++,
396
+ action: 'Create New Design',
397
+ f360: isSheetMetal
398
+ ? 'File โ†’ New Design โ†’ Switch to SHEET METAL tab in toolbar'
399
+ : 'File โ†’ New Design (Parametric design mode)',
400
+ cycleCAD: isSheetMetal ? 'newSheetMetalPart()' : 'newPart()',
401
+ tip: `Original: Inventor ${parsedData.ufrxDoc?.savedFrom || 'unknown version'}`,
402
+ icon: '๐Ÿ“„'
403
+ });
404
+
405
+ // Step 2: Set up parameters
406
+ if (parsedData.parameters.length > 0) {
407
+ steps.push({
408
+ step: stepNum++,
409
+ action: 'Define User Parameters',
410
+ f360: `Modify โ†’ Change Parameters โ†’ Add each:\n${parsedData.parameters.map(p => ` โ€ข ${p.name} = ${p.id || '?'} mm`).join('\n')}`,
411
+ cycleCAD: parsedData.parameters.map(p => `setParam('${p.name}', ${p.id || 0})`).join('; '),
412
+ tip: `${parsedData.parameters.length} parameters found. Set these FIRST โ€” features reference them.`,
413
+ icon: 'โš™๏ธ'
414
+ });
415
+ }
416
+
417
+ // Step 3: Set material thickness for sheet metal
418
+ if (isSheetMetal) {
419
+ const thicknessParam = parsedData.parameters.find(p =>
420
+ p.name.toLowerCase().includes('stรคrke') || p.name.toLowerCase().includes('thickness') || p.name.toLowerCase() === 't'
421
+ );
422
+ steps.push({
423
+ step: stepNum++,
424
+ action: 'Set Sheet Metal Rules',
425
+ f360: `SHEET METAL tab โ†’ Sheet Metal Rules โ†’ Thickness: ${thicknessParam ? thicknessParam.id + ' mm' : '(check parameters)'}`,
426
+ cycleCAD: `setSheetMetalRules({ thickness: ${thicknessParam?.id || 1} })`,
427
+ tip: 'Set thickness before creating any sheet metal features',
428
+ icon: '๐Ÿ“'
429
+ });
430
+ }
431
+
432
+ // Step 4+: Features in order
433
+ const features = parsedData.allFeatures || [];
434
+ const featureOrder = [
435
+ 'Sketch', '3D Sketch', 'WorkPlane', 'WorkAxis', 'WorkPoint',
436
+ 'Face', 'Extrude', 'Revolve', 'Sweep', 'Loft',
437
+ 'Flange', 'ContourFlange', 'Bend', 'Hem', 'Fold',
438
+ 'Hole', 'Cut', 'Boolean', 'Split',
439
+ 'Fillet', 'Chamfer', 'Shell', 'Thread',
440
+ 'Mirror', 'RectPattern', 'CircPattern',
441
+ 'Unfold', 'FlatPattern'
442
+ ];
443
+
444
+ // Sort features by logical build order
445
+ const sortedFeatures = [...features].sort((a, b) => {
446
+ const aIdx = featureOrder.indexOf(a.type);
447
+ const bIdx = featureOrder.indexOf(b.type);
448
+ return (aIdx === -1 ? 999 : aIdx) - (bIdx === -1 ? 999 : bIdx);
449
+ });
450
+
451
+ for (const feat of sortedFeatures) {
452
+ const plural = feat.count > 1 ? ` (ร—${feat.count})` : '';
453
+ steps.push({
454
+ step: stepNum++,
455
+ action: `${feat.icon} ${feat.type}${plural}`,
456
+ f360: `${isSheetMetal ? 'SHEET METAL' : 'SOLID'} tab โ†’ ${feat.f360}`,
457
+ cycleCAD: `${feat.cycleCAD}()`,
458
+ tip: feat.count > 1 ? `Repeat ${feat.count} times or use pattern` : '',
459
+ icon: feat.icon,
460
+ color: feat.color
461
+ });
462
+ }
463
+
464
+ // Final step: flat pattern for sheet metal
465
+ if (isSheetMetal && !features.find(f => f.type === 'FlatPattern')) {
466
+ steps.push({
467
+ step: stepNum++,
468
+ action: '๐Ÿ“‹ Create Flat Pattern',
469
+ f360: 'SHEET METAL tab โ†’ Create Flat Pattern โ†’ Verify all bends unfold correctly',
470
+ cycleCAD: 'createFlatPattern()',
471
+ tip: 'Verify flat pattern for DXF export to laser/punch',
472
+ icon: '๐Ÿ“‹'
473
+ });
474
+ }
475
+
476
+ // Verify step
477
+ steps.push({
478
+ step: stepNum++,
479
+ action: 'โœ… Verify & Compare',
480
+ f360: 'Inspect โ†’ Measure to check key dimensions match original Inventor model',
481
+ cycleCAD: 'verifyDimensions()',
482
+ tip: 'Compare with original parameters to ensure accuracy',
483
+ icon: 'โœ…'
484
+ });
485
+
486
+ return steps;
487
+ }
488
+
489
+ // ============================================================================
490
+ // ASSEMBLY RECONSTRUCTION GUIDE
491
+ // ============================================================================
492
+
493
+ function generateAssemblyGuide(parsedData) {
494
+ const steps = [];
495
+ let stepNum = 1;
496
+
497
+ steps.push({
498
+ step: stepNum++,
499
+ action: 'Create New Assembly',
500
+ f360: 'File โ†’ New Design โ†’ Start with empty component',
501
+ cycleCAD: 'newAssembly()',
502
+ tip: 'Fusion 360 uses a top-down assembly workflow',
503
+ icon: '๐Ÿ—๏ธ'
504
+ });
505
+
506
+ // Add component steps
507
+ const components = parsedData.fileRefs || [];
508
+ const iptFiles = components.filter(c => c.toLowerCase().endsWith('.ipt'));
509
+ const iamFiles = components.filter(c => c.toLowerCase().endsWith('.iam'));
510
+
511
+ if (iamFiles.length > 0) {
512
+ steps.push({
513
+ step: stepNum++,
514
+ action: `Import ${iamFiles.length} Sub-Assemblies`,
515
+ f360: iamFiles.map(f => ` โ€ข Assemble โ†’ Insert โ†’ ${f}`).join('\n'),
516
+ cycleCAD: iamFiles.map(f => `insertComponent('${f}')`).join('; '),
517
+ tip: 'Import sub-assemblies first, then individual parts',
518
+ icon: '๐Ÿ“ฆ'
519
+ });
520
+ }
521
+
522
+ if (iptFiles.length > 0) {
523
+ steps.push({
524
+ step: stepNum++,
525
+ action: `Import ${iptFiles.length} Parts`,
526
+ f360: `Assemble โ†’ Insert for each part:\n${iptFiles.slice(0, 10).map(f => ` โ€ข ${f}`).join('\n')}${iptFiles.length > 10 ? `\n โ€ข ... and ${iptFiles.length - 10} more` : ''}`,
527
+ cycleCAD: iptFiles.map(f => `insertComponent('${f}')`).join('; '),
528
+ tip: `Total: ${iptFiles.length} part files`,
529
+ icon: '๐Ÿ”ง'
530
+ });
531
+ }
532
+
533
+ // Constraint steps
534
+ const constraints = parsedData.constraints || [];
535
+ if (constraints.length > 0) {
536
+ steps.push({
537
+ step: stepNum++,
538
+ action: `Apply ${constraints.length} Joint Types`,
539
+ f360: constraints.map(c => ` โ€ข ${c.type} โ†’ Fusion 360: ${c.f360}`).join('\n'),
540
+ cycleCAD: 'applyJoints()',
541
+ tip: 'Inventor Constraints โ†’ Fusion 360 Joints. Some may need manual adjustment.',
542
+ icon: '๐Ÿ”—'
543
+ });
544
+ }
545
+
546
+ steps.push({
547
+ step: stepNum++,
548
+ action: 'โœ… Verify Assembly',
549
+ f360: 'Inspect โ†’ Interference to check for clashes',
550
+ cycleCAD: 'verifyAssembly()',
551
+ tip: 'Check for interference and verify motion',
552
+ icon: 'โœ…'
553
+ });
554
+
555
+ return steps;
556
+ }
557
+
558
+ // ============================================================================
559
+ // MAIN PARSER
560
+ // ============================================================================
561
+
562
+ export async function parseInventorFile(file) {
563
+ const buffer = await file.arrayBuffer();
564
+ const arr = new Uint8Array(buffer);
565
+
566
+ const filename = file.name;
567
+ const isIPT = filename.toLowerCase().endsWith('.ipt');
568
+ const isIAM = filename.toLowerCase().endsWith('.iam');
569
+ const fileType = isIPT ? 'ipt' : isIAM ? 'iam' : 'unknown';
570
+
571
+ const result = {
572
+ type: fileType,
573
+ filename: file.name,
574
+ fileSize: file.size,
575
+ metadata: {},
576
+ ufrxDoc: null,
577
+ allFeatures: [],
578
+ parameters: [],
579
+ fileRefs: [],
580
+ constraints: [],
581
+ reconstructionGuide: [],
582
+ rawStreams: [],
583
+ error: null
584
+ };
585
+
586
+ try {
587
+ const ole2 = parseOLE2(arr);
588
+ result.metadata = { oleVersion: ole2.version, streams: ole2.entries.length };
589
+
590
+ // 1. Parse UFRxDoc (primary feature/metadata source)
591
+ result.ufrxDoc = parseUFRxDoc(ole2);
592
+
593
+ // 2. Deep-scan all streams
594
+ const scanResult = scanAllStreams(ole2);
595
+
596
+ // 3. Merge features from UFRxDoc + stream scan (deduplicate)
597
+ const featureMap = new Map();
598
+ if (result.ufrxDoc?.features) {
599
+ for (const f of result.ufrxDoc.features) featureMap.set(f.type, f);
600
+ }
601
+ for (const f of scanResult.features) {
602
+ if (!featureMap.has(f.type) || f.count > featureMap.get(f.type).count) {
603
+ featureMap.set(f.type, f);
604
+ }
605
+ }
606
+ result.allFeatures = Array.from(featureMap.values());
607
+
608
+ // 4. Merge parameters
609
+ result.parameters = result.ufrxDoc?.parameters || [];
610
+
611
+ // 5. File references
612
+ result.fileRefs = [...new Set([...(result.ufrxDoc?.references || []), ...scanResult.fileRefs])];
613
+
614
+ // 6. Constraints
615
+ result.constraints = scanResult.constraints;
616
+
617
+ // 7. Generate reconstruction guide
618
+ if (isIAM) {
619
+ result.reconstructionGuide = generateAssemblyGuide(result);
620
+ } else {
621
+ result.reconstructionGuide = generateFusion360Guide(result);
622
+ }
623
+
624
+ // 8. Raw stream info
625
+ result.rawStreams = ole2.getAllStreams().map(e => ({ name: e.name, size: e.size }));
626
+
627
+ } catch (error) {
628
+ result.error = error.message;
629
+ }
630
+
631
+ return result;
632
+ }
633
+
634
+ // ============================================================================
635
+ // UI PANEL (v2 with Reconstruction Guide tab)
636
+ // ============================================================================
637
+
638
+ export function createInventorPanel(onFileLoaded = null) {
639
+ // Remove existing panel
640
+ const existing = document.getElementById('inventor-panel');
641
+ if (existing) existing.remove();
642
+
643
+ const panel = document.createElement('div');
644
+ panel.id = 'inventor-panel';
645
+ panel.style.cssText = `
646
+ position: fixed;
647
+ top: 50%; left: 50%;
648
+ transform: translate(-50%, -50%);
649
+ width: min(92vw, 1100px);
650
+ height: min(92vh, 850px);
651
+ background: #1e1e1e;
652
+ border: 1px solid #3e3e42;
653
+ border-radius: 8px;
654
+ z-index: 600;
655
+ display: flex;
656
+ flex-direction: column;
657
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
658
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
659
+ color: #e0e0e0;
660
+ `;
661
+
662
+ // Header
663
+ const header = document.createElement('div');
664
+ header.style.cssText = `
665
+ padding: 14px 16px;
666
+ border-bottom: 1px solid #3e3e42;
667
+ display: flex;
668
+ justify-content: space-between;
669
+ align-items: center;
670
+ user-select: none;
671
+ cursor: move;
672
+ background: linear-gradient(135deg, #1e1e1e, #252530);
673
+ `;
674
+ header.innerHTML = `
675
+ <div style="display: flex; align-items: center; gap: 10px;">
676
+ <span style="font-size: 20px;">๐Ÿญ</span>
677
+ <div>
678
+ <h2 style="margin: 0; font-size: 15px; color: #e0e0e0;">Inventor โ†’ Fusion 360 / cycleCAD</h2>
679
+ <div style="font-size: 11px; color: #8b949e; margin-top: 2px;">Reverse-engineer native .ipt/.iam files</div>
680
+ </div>
681
+ </div>
682
+ `;
683
+ const closeBtn = document.createElement('button');
684
+ closeBtn.textContent = 'โœ•';
685
+ closeBtn.style.cssText = `background:transparent;border:none;color:#8b949e;font-size:20px;cursor:pointer;padding:4px;`;
686
+ closeBtn.onclick = () => panel.remove();
687
+ header.appendChild(closeBtn);
688
+ panel.appendChild(header);
689
+
690
+ // Content area (drop zone initially)
691
+ const content = document.createElement('div');
692
+ content.style.cssText = `
693
+ flex: 1;
694
+ display: flex;
695
+ align-items: center;
696
+ justify-content: center;
697
+ min-height: 0;
698
+ `;
699
+
700
+ const dropZone = document.createElement('div');
701
+ dropZone.style.cssText = `
702
+ width: calc(100% - 32px);
703
+ height: calc(100% - 32px);
704
+ border: 2px dashed #3e3e42;
705
+ border-radius: 8px;
706
+ display: flex;
707
+ flex-direction: column;
708
+ align-items: center;
709
+ justify-content: center;
710
+ cursor: pointer;
711
+ transition: all 0.2s;
712
+ background: #252526;
713
+ gap: 12px;
714
+ `;
715
+ // Hidden file input (persistent, not dynamic โ€” works reliably across browsers)
716
+ const fileInput = document.createElement('input');
717
+ fileInput.type = 'file';
718
+ fileInput.accept = '.ipt,.iam';
719
+ fileInput.id = 'inventor-file-input';
720
+ fileInput.style.cssText = 'position:absolute;width:0;height:0;opacity:0;pointer-events:none;';
721
+ fileInput.onchange = async (e) => {
722
+ if (e.target.files.length > 0) await showResults(e.target.files[0], content, panel, onFileLoaded);
723
+ };
724
+
725
+ dropZone.innerHTML = `
726
+ <div style="font-size: 48px;">๐Ÿญ</div>
727
+ <div style="color: #e0e0e0; font-size: 15px; font-weight: 500;">Drop Inventor File Here</div>
728
+ <div style="color: #8b949e; font-size: 12px;">Supports .ipt (Part) and .iam (Assembly)</div>
729
+ `;
730
+
731
+ // Browse button using <label> wrapping hidden input โ€” reliable click-to-browse
732
+ const browseLabel = document.createElement('label');
733
+ browseLabel.setAttribute('for', 'inventor-file-input');
734
+ browseLabel.style.cssText = `
735
+ display: inline-block;
736
+ margin-top: 8px;
737
+ padding: 8px 24px;
738
+ background: rgba(255,140,0,0.15);
739
+ border: 1px solid rgba(255,140,0,0.4);
740
+ border-radius: 6px;
741
+ color: #ff8c00;
742
+ font-size: 13px;
743
+ font-weight: 500;
744
+ cursor: pointer;
745
+ transition: all 0.2s;
746
+ `;
747
+ browseLabel.textContent = 'Browse Files...';
748
+ browseLabel.onmouseenter = () => { browseLabel.style.background = 'rgba(255,140,0,0.25)'; };
749
+ browseLabel.onmouseleave = () => { browseLabel.style.background = 'rgba(255,140,0,0.15)'; };
750
+
751
+ dropZone.appendChild(fileInput);
752
+ dropZone.appendChild(browseLabel);
753
+
754
+ // Sample DUO files section
755
+ const samplesDiv = document.createElement('div');
756
+ samplesDiv.style.cssText = `
757
+ margin-top: 16px;
758
+ padding-top: 16px;
759
+ border-top: 1px solid #3e3e42;
760
+ text-align: center;
761
+ width: 100%;
762
+ `;
763
+ samplesDiv.innerHTML = `
764
+ <div style="color:#8b949e;font-size:11px;margin-bottom:10px;">DUO Sample Files</div>
765
+ <div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap;">
766
+ <button class="inv-sample-btn" data-file="Leistenbuerstenblech.ipt" style="padding:6px 14px;background:rgba(88,166,255,0.1);border:1px solid rgba(88,166,255,0.3);border-radius:5px;color:#58a6ff;font-size:12px;cursor:pointer;">Leistenbuerstenblech.ipt</button>
767
+ <button class="inv-sample-btn" data-file="TraegerHoehe1.ipt" style="padding:6px 14px;background:rgba(88,166,255,0.1);border:1px solid rgba(88,166,255,0.3);border-radius:5px;color:#58a6ff;font-size:12px;cursor:pointer;">TraegerHoehe1.ipt</button>
768
+ <button class="inv-sample-btn" data-file="Rahmen_Seite.iam" style="padding:6px 14px;background:rgba(88,166,255,0.1);border:1px solid rgba(88,166,255,0.3);border-radius:5px;color:#58a6ff;font-size:12px;cursor:pointer;">Rahmen_Seite.iam</button>
769
+ </div>
770
+ `;
771
+ dropZone.appendChild(samplesDiv);
772
+
773
+ // Sample button click handlers โ€” fetch from samples/ directory
774
+ samplesDiv.querySelectorAll('.inv-sample-btn').forEach(btn => {
775
+ btn.onmouseenter = () => { btn.style.background = 'rgba(88,166,255,0.2)'; };
776
+ btn.onmouseleave = () => { btn.style.background = 'rgba(88,166,255,0.1)'; };
777
+ btn.onclick = async (e) => {
778
+ e.stopPropagation();
779
+ const fileName = btn.dataset.file;
780
+ btn.textContent = 'Loading...';
781
+ btn.style.opacity = '0.6';
782
+ try {
783
+ const resp = await fetch(`samples/${fileName}`);
784
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
785
+ const blob = await resp.blob();
786
+ const file = new File([blob], fileName);
787
+ await showResults(file, content, panel, onFileLoaded);
788
+ } catch (err) {
789
+ btn.textContent = `Failed: ${err.message}`;
790
+ btn.style.color = '#f85149';
791
+ btn.style.borderColor = 'rgba(248,81,73,0.3)';
792
+ setTimeout(() => {
793
+ btn.textContent = fileName;
794
+ btn.style.color = '#58a6ff';
795
+ btn.style.borderColor = 'rgba(88,166,255,0.3)';
796
+ btn.style.opacity = '1';
797
+ }, 2000);
798
+ }
799
+ };
800
+ });
801
+
802
+ dropZone.ondragover = (e) => { e.preventDefault(); dropZone.style.borderColor = '#ff8c00'; dropZone.style.background = '#2a2520'; };
803
+ dropZone.ondragleave = () => { dropZone.style.borderColor = '#3e3e42'; dropZone.style.background = '#252526'; };
804
+ dropZone.ondrop = async (e) => {
805
+ e.preventDefault();
806
+ dropZone.style.borderColor = '#3e3e42';
807
+ const files = Array.from(e.dataTransfer.files).filter(f => /\.(ipt|iam)$/i.test(f.name));
808
+ if (files.length > 0) await showResults(files[0], content, panel, onFileLoaded);
809
+ };
810
+ // Prevent dropZone click from interfering with label/button clicks
811
+ dropZone.style.cursor = 'default';
812
+
813
+ content.appendChild(dropZone);
814
+ panel.appendChild(content);
815
+
816
+ // Draggable
817
+ makeDraggable(panel, header);
818
+
819
+ // Auto-show
820
+ document.body.appendChild(panel);
821
+
822
+ return {
823
+ element: panel,
824
+ show: () => document.body.appendChild(panel),
825
+ hide: () => panel.remove(),
826
+ };
827
+ }
828
+
829
+ function makeDraggable(el, handle) {
830
+ let ox = 0, oy = 0;
831
+ handle.onmousedown = (e) => {
832
+ if (e.target.tagName === 'BUTTON') return;
833
+ const rect = el.getBoundingClientRect();
834
+ ox = e.clientX - rect.left; oy = e.clientY - rect.top;
835
+ const move = (ev) => { el.style.transform = 'none'; el.style.left = (ev.clientX - ox) + 'px'; el.style.top = (ev.clientY - oy) + 'px'; };
836
+ const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); };
837
+ document.addEventListener('mousemove', move);
838
+ document.addEventListener('mouseup', up);
839
+ };
840
+ }
841
+
842
+ async function showResults(file, container, panel, callback) {
843
+ container.innerHTML = '<div style="color: #ff8c00; font-size: 14px;">โณ Parsing Inventor file...</div>';
844
+
845
+ const data = await parseInventorFile(file);
846
+
847
+ container.innerHTML = '';
848
+ container.style.cssText = 'flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden;';
849
+
850
+ // Tab bar
851
+ const tabBar = document.createElement('div');
852
+ tabBar.style.cssText = 'display:flex;border-bottom:1px solid #3e3e42;background:#1a1a1a;flex-shrink:0;';
853
+
854
+ const isIAM = data.type === 'iam';
855
+ const tabNames = ['๐Ÿ” Overview', '๐Ÿ”ง Rebuild Guide', '๐Ÿ“ฆ Features', 'โš™๏ธ Parameters', ...(isIAM ? ['๐Ÿ—๏ธ Assembly'] : []), '๐Ÿ“Š Raw Data'];
856
+ const tabPanels = {};
857
+
858
+ tabNames.forEach((name, i) => {
859
+ const btn = document.createElement('button');
860
+ btn.textContent = name;
861
+ btn.style.cssText = `flex:1;padding:10px 6px;background:transparent;border:none;border-bottom:2px solid transparent;color:#8b949e;cursor:pointer;font-size:11px;white-space:nowrap;`;
862
+ btn.onclick = () => {
863
+ Object.values(tabPanels).forEach(p => p.style.display = 'none');
864
+ tabPanels[name].style.display = 'flex';
865
+ Array.from(tabBar.children).forEach(b => { b.style.borderBottomColor = 'transparent'; b.style.color = '#8b949e'; });
866
+ btn.style.borderBottomColor = '#ff8c00'; btn.style.color = '#e0e0e0';
867
+ };
868
+ if (i === 0) { btn.style.borderBottomColor = '#ff8c00'; btn.style.color = '#e0e0e0'; }
869
+ tabBar.appendChild(btn);
870
+
871
+ const p = document.createElement('div');
872
+ p.style.cssText = `flex:1;overflow-y:auto;padding:16px;display:${i === 0 ? 'flex' : 'none'};flex-direction:column;gap:8px;min-height:0;font-size:12px;`;
873
+ tabPanels[name] = p;
874
+ });
875
+
876
+ container.appendChild(tabBar);
877
+
878
+ // ====== OVERVIEW TAB ======
879
+ const overview = tabPanels['๐Ÿ” Overview'];
880
+ const partType = data.ufrxDoc?.partType || 'Standard';
881
+ const templateIcon = TEMPLATE_MAP[Object.keys(TEMPLATE_MAP).find(k => partType.includes(k))]?.icon || '๐Ÿ“ฆ';
882
+ overview.innerHTML = `
883
+ <div style="background:#252526;padding:14px;border-radius:6px;border-left:3px solid #ff8c00;">
884
+ <div style="font-size:16px;font-weight:600;margin-bottom:8px;">${templateIcon} ${data.filename}</div>
885
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;color:#b0b0b0;">
886
+ <div>Type: <strong style="color:#e0e0e0;">${data.type.toUpperCase()} โ€” ${partType}</strong></div>
887
+ <div>Size: <strong style="color:#e0e0e0;">${(data.fileSize / 1024).toFixed(1)} KB</strong></div>
888
+ <div>OLE Version: <strong style="color:#e0e0e0;">${data.metadata.oleVersion}</strong></div>
889
+ <div>Template: <strong style="color:#e0e0e0;">${data.ufrxDoc?.template || 'โ€”'}</strong></div>
890
+ <div>Created with: <strong style="color:#e0e0e0;">${data.ufrxDoc?.savedFrom || 'โ€”'}</strong></div>
891
+ <div>Saved on: <strong style="color:#e0e0e0;">${data.ufrxDoc?.savedOn || 'โ€”'}</strong></div>
892
+ <div style="grid-column:1/3;">Path: <strong style="color:#e0e0e0;word-break:break-all;font-size:10px;">${data.ufrxDoc?.originalPath || 'โ€”'}</strong></div>
893
+ </div>
894
+ </div>
895
+ <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;">
896
+ <div style="background:#252526;padding:12px;border-radius:6px;text-align:center;">
897
+ <div style="font-size:24px;color:#3fb950;">${data.allFeatures.length}</div>
898
+ <div style="color:#8b949e;font-size:11px;">Features</div>
899
+ </div>
900
+ <div style="background:#252526;padding:12px;border-radius:6px;text-align:center;">
901
+ <div style="font-size:24px;color:#58a6ff;">${data.parameters.length}</div>
902
+ <div style="color:#8b949e;font-size:11px;">Parameters</div>
903
+ </div>
904
+ <div style="background:#252526;padding:12px;border-radius:6px;text-align:center;">
905
+ <div style="font-size:24px;color:#d29922;">${data.fileRefs.length}</div>
906
+ <div style="color:#8b949e;font-size:11px;">File Refs</div>
907
+ </div>
908
+ <div style="background:#252526;padding:12px;border-radius:6px;text-align:center;">
909
+ <div style="font-size:24px;color:#a371f7;">${data.rawStreams.length}</div>
910
+ <div style="color:#8b949e;font-size:11px;">Streams</div>
911
+ </div>
912
+ </div>
913
+ ${data.error ? `<div style="background:#3d1f1f;padding:10px;border-radius:6px;color:#f85149;">โš ๏ธ ${data.error}</div>` : ''}
914
+ `;
915
+
916
+ // ====== REBUILD GUIDE TAB ======
917
+ const guideTab = tabPanels['๐Ÿ”ง Rebuild Guide'];
918
+ if (data.reconstructionGuide.length > 0) {
919
+ // Header
920
+ const guideHeader = document.createElement('div');
921
+ guideHeader.style.cssText = 'background:linear-gradient(135deg,#1a2030,#252540);padding:12px;border-radius:6px;margin-bottom:4px;';
922
+ guideHeader.innerHTML = `
923
+ <div style="font-size:14px;font-weight:600;color:#ff8c00;">Step-by-Step Reconstruction</div>
924
+ <div style="font-size:11px;color:#8b949e;margin-top:4px;">Recreate this ${partType} in Fusion 360 (free) or cycleCAD</div>
925
+ `;
926
+ guideTab.appendChild(guideHeader);
927
+
928
+ // Steps
929
+ data.reconstructionGuide.forEach(step => {
930
+ const stepEl = document.createElement('div');
931
+ stepEl.style.cssText = `
932
+ background: #252526;
933
+ padding: 12px;
934
+ border-radius: 6px;
935
+ border-left: 3px solid ${step.color || '#ff8c00'};
936
+ `;
937
+ stepEl.innerHTML = `
938
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
939
+ <span style="background:#ff8c00;color:#000;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:700;">Step ${step.step}</span>
940
+ <strong>${step.action}</strong>
941
+ </div>
942
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:6px;">
943
+ <div style="background:#1a2520;padding:8px;border-radius:4px;">
944
+ <div style="color:#3fb950;font-size:10px;font-weight:600;margin-bottom:4px;">FUSION 360</div>
945
+ <div style="white-space:pre-wrap;font-size:11px;color:#b0b0b0;">${step.f360}</div>
946
+ </div>
947
+ <div style="background:#1a1a2a;padding:8px;border-radius:4px;">
948
+ <div style="color:#58a6ff;font-size:10px;font-weight:600;margin-bottom:4px;">cycleCAD</div>
949
+ <div style="font-family:monospace;font-size:11px;color:#b0b0b0;">${step.cycleCAD}</div>
950
+ </div>
951
+ </div>
952
+ ${step.tip ? `<div style="color:#8b949e;font-size:10px;margin-top:6px;font-style:italic;">๐Ÿ’ก ${step.tip}</div>` : ''}
953
+ `;
954
+ guideTab.appendChild(stepEl);
955
+ });
956
+
957
+ // Export button
958
+ const exportGuideBtn = document.createElement('button');
959
+ exportGuideBtn.textContent = '๐Ÿ“‹ Export Rebuild Guide as HTML';
960
+ exportGuideBtn.style.cssText = 'margin-top:8px;padding:10px;background:#ff8c00;border:none;color:#000;border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;';
961
+ exportGuideBtn.onclick = () => exportGuideHTML(data);
962
+ guideTab.appendChild(exportGuideBtn);
963
+ } else {
964
+ guideTab.innerHTML = '<div style="color:#8b949e;">No reconstruction guide available</div>';
965
+ }
966
+
967
+ // ====== FEATURES TAB ======
968
+ const featTab = tabPanels['๐Ÿ“ฆ Features'];
969
+ if (data.allFeatures.length > 0) {
970
+ data.allFeatures.forEach(f => {
971
+ const el = document.createElement('div');
972
+ el.style.cssText = `padding:10px;background:#252526;border-left:3px solid ${f.color};border-radius:4px;`;
973
+ el.innerHTML = `
974
+ <div style="display:flex;justify-content:space-between;align-items:center;">
975
+ <strong>${f.icon} ${f.type}</strong>
976
+ <span style="color:#8b949e;font-size:10px;">ร—${f.count} | ${f.source}</span>
977
+ </div>
978
+ <div style="margin-top:4px;font-size:11px;color:#8b949e;">
979
+ Fusion 360: <span style="color:#3fb950;">${f.f360}</span> ยท
980
+ cycleCAD: <code style="color:#58a6ff;">${f.cycleCAD}()</code>
981
+ </div>
982
+ `;
983
+ featTab.appendChild(el);
984
+ });
985
+ } else {
986
+ featTab.innerHTML = '<div style="color:#8b949e;">No features detected in binary streams. This file may use compressed geometry.</div>';
987
+ }
988
+
989
+ // ====== PARAMETERS TAB ======
990
+ const paramTab = tabPanels['โš™๏ธ Parameters'];
991
+ if (data.parameters.length > 0) {
992
+ const table = document.createElement('table');
993
+ table.style.cssText = 'width:100%;border-collapse:collapse;';
994
+ table.innerHTML = `
995
+ <thead><tr style="border-bottom:1px solid #3e3e42;">
996
+ <th style="text-align:left;padding:8px;color:#ff8c00;">Name</th>
997
+ <th style="text-align:right;padding:8px;color:#ff8c00;">ID/Value</th>
998
+ <th style="text-align:left;padding:8px;color:#ff8c00;">Source</th>
999
+ </tr></thead>
1000
+ <tbody>${data.parameters.map(p => `
1001
+ <tr style="border-bottom:1px solid #2a2a2a;">
1002
+ <td style="padding:6px;"><code>${p.name}</code></td>
1003
+ <td style="text-align:right;padding:6px;">${p.id ?? p.value ?? '?'}</td>
1004
+ <td style="padding:6px;color:#8b949e;font-size:10px;">${p.source || 'โ€”'}</td>
1005
+ </tr>
1006
+ `).join('')}</tbody>
1007
+ `;
1008
+ paramTab.appendChild(table);
1009
+ } else {
1010
+ paramTab.innerHTML = '<div style="color:#8b949e;">No parameters extracted. Parameters may be in compressed streams.</div>';
1011
+ }
1012
+
1013
+ // ====== ASSEMBLY TAB (if .iam) ======
1014
+ if (isIAM && tabPanels['๐Ÿ—๏ธ Assembly']) {
1015
+ const asmTab = tabPanels['๐Ÿ—๏ธ Assembly'];
1016
+ if (data.fileRefs.length > 0) {
1017
+ data.fileRefs.forEach(ref => {
1018
+ const el = document.createElement('div');
1019
+ const isAsm = ref.toLowerCase().endsWith('.iam');
1020
+ el.style.cssText = `padding:8px;background:#252526;border-radius:4px;border-left:3px solid ${isAsm ? '#d29922' : '#58a6ff'};`;
1021
+ el.innerHTML = `${isAsm ? '๐Ÿ—๏ธ' : '๐Ÿ”ง'} <strong>${ref}</strong> <span style="color:#8b949e;font-size:10px;">${isAsm ? 'Assembly' : 'Part'}</span>`;
1022
+ asmTab.appendChild(el);
1023
+ });
1024
+ }
1025
+ if (data.constraints.length > 0) {
1026
+ const conTitle = document.createElement('div');
1027
+ conTitle.style.cssText = 'margin-top:12px;font-weight:600;color:#ff8c00;';
1028
+ conTitle.textContent = 'Constraints โ†’ Fusion 360 Joints:';
1029
+ asmTab.appendChild(conTitle);
1030
+ data.constraints.forEach(c => {
1031
+ const el = document.createElement('div');
1032
+ el.style.cssText = 'padding:6px;background:#252526;border-radius:4px;margin-top:4px;';
1033
+ el.innerHTML = `๐Ÿ”— ${c.type} โ†’ <span style="color:#3fb950;">${c.f360}</span>`;
1034
+ asmTab.appendChild(el);
1035
+ });
1036
+ }
1037
+ }
1038
+
1039
+ // ====== RAW DATA TAB ======
1040
+ const rawTab = tabPanels['๐Ÿ“Š Raw Data'];
1041
+ rawTab.innerHTML = `
1042
+ <div style="display:flex;flex-wrap:wrap;gap:6px;">
1043
+ ${data.rawStreams.map(s => `
1044
+ <div style="background:#252526;padding:6px 10px;border-radius:4px;font-size:11px;">
1045
+ <strong>${s.name}</strong> <span style="color:#8b949e;">(${(s.size/1024).toFixed(1)} KB)</span>
1046
+ </div>
1047
+ `).join('')}
1048
+ </div>
1049
+ `;
1050
+
1051
+ // Add all tab panels
1052
+ for (const p of Object.values(tabPanels)) {
1053
+ container.appendChild(p);
1054
+ }
1055
+
1056
+ // Footer with export
1057
+ const footer = document.createElement('div');
1058
+ footer.style.cssText = 'padding:10px 16px;border-top:1px solid #3e3e42;display:flex;gap:8px;justify-content:flex-end;flex-shrink:0;';
1059
+
1060
+ const jsonBtn = document.createElement('button');
1061
+ jsonBtn.textContent = '๐Ÿ’พ Export JSON';
1062
+ jsonBtn.style.cssText = 'padding:6px 14px;background:#3e3e42;border:none;color:#e0e0e0;border-radius:4px;cursor:pointer;font-size:11px;';
1063
+ jsonBtn.onclick = () => {
1064
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
1065
+ const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
1066
+ a.download = `${data.filename}.analysis.json`; a.click();
1067
+ };
1068
+ footer.appendChild(jsonBtn);
1069
+
1070
+ const htmlBtn = document.createElement('button');
1071
+ htmlBtn.textContent = '๐Ÿ“„ Export Rebuild Guide';
1072
+ htmlBtn.style.cssText = 'padding:6px 14px;background:#ff8c00;border:none;color:#000;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600;';
1073
+ htmlBtn.onclick = () => exportGuideHTML(data);
1074
+ footer.appendChild(htmlBtn);
1075
+
1076
+ container.appendChild(footer);
1077
+
1078
+ if (callback) callback(data);
1079
+ }
1080
+
1081
+ function exportGuideHTML(data) {
1082
+ const partType = data.ufrxDoc?.partType || 'Standard';
1083
+ const html = `<!DOCTYPE html>
1084
+ <html lang="en"><head><meta charset="UTF-8"><title>Rebuild Guide: ${data.filename}</title>
1085
+ <style>
1086
+ body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 900px; margin: 40px auto; padding: 20px; background: #0d1117; color: #e0e0e0; }
1087
+ h1 { color: #ff8c00; border-bottom: 2px solid #ff8c00; padding-bottom: 10px; }
1088
+ .meta { background: #161b22; padding: 16px; border-radius: 8px; margin-bottom: 20px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
1089
+ .step { background: #161b22; padding: 16px; border-radius: 8px; margin-bottom: 12px; border-left: 4px solid #ff8c00; }
1090
+ .step-num { background: #ff8c00; color: #000; padding: 2px 10px; border-radius: 12px; font-weight: 700; font-size: 12px; }
1091
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }
1092
+ .f360 { background: #0d2818; padding: 10px; border-radius: 6px; }
1093
+ .f360 h4 { color: #3fb950; margin: 0 0 6px 0; font-size: 11px; }
1094
+ .cad { background: #0d0d28; padding: 10px; border-radius: 6px; }
1095
+ .cad h4 { color: #58a6ff; margin: 0 0 6px 0; font-size: 11px; }
1096
+ .tip { color: #8b949e; font-size: 11px; margin-top: 8px; font-style: italic; }
1097
+ code { font-family: 'SF Mono', Consolas, monospace; font-size: 12px; }
1098
+ </style></head><body>
1099
+ <h1>๐Ÿญ Rebuild Guide: ${data.filename}</h1>
1100
+ <div class="meta">
1101
+ <div>Type: <strong>${data.type.toUpperCase()} โ€” ${partType}</strong></div>
1102
+ <div>Features: <strong>${data.allFeatures.length}</strong></div>
1103
+ <div>Parameters: <strong>${data.parameters.length}</strong></div>
1104
+ <div>Created with: <strong>${data.ufrxDoc?.savedFrom || 'โ€”'}</strong></div>
1105
+ </div>
1106
+ ${data.reconstructionGuide.map(s => `
1107
+ <div class="step">
1108
+ <span class="step-num">Step ${s.step}</span> <strong>${s.action}</strong>
1109
+ <div class="grid">
1110
+ <div class="f360"><h4>FUSION 360 (Free)</h4><div style="white-space:pre-wrap;">${s.f360}</div></div>
1111
+ <div class="cad"><h4>cycleCAD (Web)</h4><code>${s.cycleCAD}</code></div>
1112
+ </div>
1113
+ ${s.tip ? `<div class="tip">๐Ÿ’ก ${s.tip}</div>` : ''}
1114
+ </div>`).join('')}
1115
+ <footer style="text-align:center;color:#8b949e;margin-top:40px;font-size:11px;">
1116
+ Generated by cycleCAD Inventor Parser ยท ${new Date().toISOString().split('T')[0]}
1117
+ </footer></body></html>`;
1118
+
1119
+ const blob = new Blob([html], { type: 'text/html' });
1120
+ const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
1121
+ a.download = `${data.filename}_rebuild_guide.html`; a.click();
1122
+ }
1123
+
1124
+ // ============================================================================
1125
+ // EXPORTS
1126
+ // ============================================================================
1127
+
1128
+ export { parseOLE2, parseUFRxDoc, scanAllStreams, generateFusion360Guide, generateAssemblyGuide };
1129
+
1130
+ export default {
1131
+ parseInventorFile,
1132
+ createInventorPanel,
1133
+ parseOLE2,
1134
+ parseUFRxDoc,
1135
+ scanAllStreams,
1136
+ generateFusion360Guide,
1137
+ generateAssemblyGuide
1138
+ };