cyclecad 0.1.3 → 0.1.4
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 +233 -0
- package/DUO-MANIFEST-README.md +233 -0
- package/MASTERPLAN.md +182 -0
- package/app/duo-manifest-demo.html +337 -0
- package/app/duo-manifest.json +7375 -0
- package/app/index.html +1167 -23
- package/app/js/app.js +79 -9
- package/app/js/assembly-resolver.js +477 -0
- package/app/js/operations.js +501 -112
- package/app/js/project-browser.js +741 -0
- package/app/js/project-loader.js +579 -0
- package/app/js/rebuild-guide.js +743 -0
- package/app/js/viewport.js +24 -0
- package/package.json +2 -2
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* project-loader.js - ES Module for loading Inventor projects
|
|
3
|
+
*
|
|
4
|
+
* Handles parsing .ipj files, indexing project contents, and providing
|
|
5
|
+
* a folder-based file selection UI for Inventor project structures.
|
|
6
|
+
*
|
|
7
|
+
* Exports:
|
|
8
|
+
* - loadProject(directoryHandle)
|
|
9
|
+
* - parseIPJ(buffer)
|
|
10
|
+
* - indexFiles(entries)
|
|
11
|
+
* - getProjectStats()
|
|
12
|
+
* - getFileByPath(path)
|
|
13
|
+
* - showFolderPicker()
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// FILE TYPE CONSTANTS
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const INVENTOR_EXTENSIONS = {
|
|
21
|
+
IPT: '.ipt', // Part files
|
|
22
|
+
IAM: '.iam', // Assembly files
|
|
23
|
+
IPJ: '.ipj', // Project files
|
|
24
|
+
IDW: '.idw', // Drawing files
|
|
25
|
+
DWG: '.dwg', // AutoCAD drawing files
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const FILE_CATEGORIES = {
|
|
29
|
+
PART: 'part',
|
|
30
|
+
ASSEMBLY: 'assembly',
|
|
31
|
+
PROJECT: 'project',
|
|
32
|
+
DRAWING: 'drawing',
|
|
33
|
+
OTHER: 'other',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const PART_CLASSIFICATIONS = {
|
|
37
|
+
CUSTOM: 'custom',
|
|
38
|
+
STANDARD: 'standard',
|
|
39
|
+
BUYOUT: 'buyout',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// PROJECT STATE
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
let projectState = {
|
|
47
|
+
name: '',
|
|
48
|
+
ipj: null,
|
|
49
|
+
files: new Map(),
|
|
50
|
+
tree: null,
|
|
51
|
+
stats: {
|
|
52
|
+
parts: 0,
|
|
53
|
+
assemblies: 0,
|
|
54
|
+
drawings: 0,
|
|
55
|
+
total: 0,
|
|
56
|
+
byClassification: {
|
|
57
|
+
custom: 0,
|
|
58
|
+
standard: 0,
|
|
59
|
+
buyout: 0,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// UTF-16 DECODING FOR IPJ FILES
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Decode UTF-16 little-endian buffer to string
|
|
70
|
+
* @param {ArrayBuffer} buffer
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
function decodeUTF16LE(buffer) {
|
|
74
|
+
const view = new Uint16Array(buffer);
|
|
75
|
+
let str = '';
|
|
76
|
+
for (let i = 0; i < view.length; i++) {
|
|
77
|
+
str += String.fromCharCode(view[i]);
|
|
78
|
+
}
|
|
79
|
+
return str;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// IPJ FILE PARSING
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse an Inventor Project (.ipj) file
|
|
88
|
+
* IPJ files are UTF-16 XML documents containing project configuration
|
|
89
|
+
*
|
|
90
|
+
* @param {ArrayBuffer} buffer - Raw file buffer
|
|
91
|
+
* @returns {Object} Parsed project info { workspace, contentCenter, options }
|
|
92
|
+
*/
|
|
93
|
+
export function parseIPJ(buffer) {
|
|
94
|
+
try {
|
|
95
|
+
const xmlString = decodeUTF16LE(buffer);
|
|
96
|
+
|
|
97
|
+
// Extract workspace path from <Workspace>.\Workspaces\...\</Workspace>
|
|
98
|
+
const workspaceMatch = xmlString.match(/<Workspace>([^<]+)<\/Workspace>/i);
|
|
99
|
+
const workspace = workspaceMatch ? workspaceMatch[1].trim() : null;
|
|
100
|
+
|
|
101
|
+
// Extract Content Center path
|
|
102
|
+
const ccMatch = xmlString.match(/<ContentCenterPath>([^<]+)<\/ContentCenterPath>/i);
|
|
103
|
+
const contentCenter = ccMatch ? ccMatch[1].trim() : null;
|
|
104
|
+
|
|
105
|
+
// Extract project options
|
|
106
|
+
const options = {};
|
|
107
|
+
const oldVersionsMatch = xmlString.match(/<OldVersionsToKeep>(\d+)<\/OldVersionsToKeep>/i);
|
|
108
|
+
if (oldVersionsMatch) {
|
|
109
|
+
options.oldVersionsToKeep = parseInt(oldVersionsMatch[1], 10);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const uniqueFilenamesMatch = xmlString.match(/<UsingUniqueFilenames>(true|false)<\/UsingUniqueFilenames>/i);
|
|
113
|
+
if (uniqueFilenamesMatch) {
|
|
114
|
+
options.usingUniqueFilenames = uniqueFilenamesMatch[1].toLowerCase() === 'true';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
workspace,
|
|
119
|
+
contentCenter,
|
|
120
|
+
options,
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Error parsing IPJ file:', error);
|
|
124
|
+
return { workspace: null, contentCenter: null, options: {} };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// FILE CLASSIFICATION
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Classify a part file based on its path location
|
|
134
|
+
* @param {string} path - File path
|
|
135
|
+
* @returns {string} Classification: 'custom', 'standard', or 'buyout'
|
|
136
|
+
*/
|
|
137
|
+
function classifyPart(path) {
|
|
138
|
+
const lowerPath = path.toLowerCase();
|
|
139
|
+
|
|
140
|
+
if (lowerPath.includes('content center') || lowerPath.includes('libraries')) {
|
|
141
|
+
return PART_CLASSIFICATIONS.STANDARD;
|
|
142
|
+
}
|
|
143
|
+
if (lowerPath.includes('zukaufteile') || lowerPath.includes('buyout')) {
|
|
144
|
+
return PART_CLASSIFICATIONS.BUYOUT;
|
|
145
|
+
}
|
|
146
|
+
return PART_CLASSIFICATIONS.CUSTOM;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get file extension and categorize file type
|
|
151
|
+
* @param {string} filename
|
|
152
|
+
* @returns {Object} { ext, category }
|
|
153
|
+
*/
|
|
154
|
+
function categorizeFile(filename) {
|
|
155
|
+
const ext = filename.slice(filename.lastIndexOf('.')).toLowerCase();
|
|
156
|
+
|
|
157
|
+
let category = FILE_CATEGORIES.OTHER;
|
|
158
|
+
if (ext === INVENTOR_EXTENSIONS.IPT) category = FILE_CATEGORIES.PART;
|
|
159
|
+
else if (ext === INVENTOR_EXTENSIONS.IAM) category = FILE_CATEGORIES.ASSEMBLY;
|
|
160
|
+
else if (ext === INVENTOR_EXTENSIONS.IPJ) category = FILE_CATEGORIES.PROJECT;
|
|
161
|
+
else if (ext === INVENTOR_EXTENSIONS.IDW || ext === INVENTOR_EXTENSIONS.DWG) {
|
|
162
|
+
category = FILE_CATEGORIES.DRAWING;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { ext, category };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// FILE TREE BUILDING
|
|
170
|
+
// ============================================================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build a hierarchical tree from flat file entries
|
|
174
|
+
* @param {Map} filesMap - Map of path to file metadata
|
|
175
|
+
* @returns {Object} Tree root node
|
|
176
|
+
*/
|
|
177
|
+
function buildFileTree(filesMap) {
|
|
178
|
+
const root = {
|
|
179
|
+
name: 'Project Root',
|
|
180
|
+
type: 'folder',
|
|
181
|
+
path: '',
|
|
182
|
+
children: [],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const nodeMap = new Map();
|
|
186
|
+
nodeMap.set('', root);
|
|
187
|
+
|
|
188
|
+
// Sort paths for consistent ordering
|
|
189
|
+
const sortedPaths = Array.from(filesMap.keys()).sort();
|
|
190
|
+
|
|
191
|
+
for (const path of sortedPaths) {
|
|
192
|
+
const file = filesMap.get(path);
|
|
193
|
+
const parts = path.split('/').filter(Boolean);
|
|
194
|
+
|
|
195
|
+
let currentPath = '';
|
|
196
|
+
let currentParent = root;
|
|
197
|
+
|
|
198
|
+
// Navigate/create folder hierarchy
|
|
199
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
200
|
+
const folderName = parts[i];
|
|
201
|
+
currentPath = (currentPath ? currentPath + '/' : '') + folderName;
|
|
202
|
+
|
|
203
|
+
if (!nodeMap.has(currentPath)) {
|
|
204
|
+
const folderNode = {
|
|
205
|
+
name: folderName,
|
|
206
|
+
type: 'folder',
|
|
207
|
+
path: currentPath,
|
|
208
|
+
children: [],
|
|
209
|
+
};
|
|
210
|
+
nodeMap.set(currentPath, folderNode);
|
|
211
|
+
currentParent.children.push(folderNode);
|
|
212
|
+
}
|
|
213
|
+
currentParent = nodeMap.get(currentPath);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Add file node
|
|
217
|
+
const fileNode = {
|
|
218
|
+
name: file.name,
|
|
219
|
+
type: file.category,
|
|
220
|
+
path: path,
|
|
221
|
+
size: file.size,
|
|
222
|
+
classification: file.classification,
|
|
223
|
+
ext: file.ext,
|
|
224
|
+
};
|
|
225
|
+
currentParent.children.push(fileNode);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return root;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// FILE INDEXING
|
|
233
|
+
// ============================================================================
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Index all files from FileSystemEntry objects (from drop/picker)
|
|
237
|
+
* Recursively traverses directory structure and builds file index
|
|
238
|
+
*
|
|
239
|
+
* @param {FileSystemEntry[]|DataTransferItemList} entries
|
|
240
|
+
* @returns {Promise<Map>} Map of path -> { name, ext, size, type, category, buffer, classification }
|
|
241
|
+
*/
|
|
242
|
+
export async function indexFiles(entries) {
|
|
243
|
+
const filesMap = new Map();
|
|
244
|
+
const stack = [];
|
|
245
|
+
|
|
246
|
+
// Initialize with top-level entries
|
|
247
|
+
for (let i = 0; i < entries.length; i++) {
|
|
248
|
+
const entry = entries[i];
|
|
249
|
+
if (entry instanceof DataTransferItem) {
|
|
250
|
+
stack.push(entry.webkitGetAsEntry());
|
|
251
|
+
} else if (entry.isDirectory) {
|
|
252
|
+
stack.push(entry);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Depth-first traversal
|
|
257
|
+
while (stack.length > 0) {
|
|
258
|
+
const entry = stack.pop();
|
|
259
|
+
|
|
260
|
+
if (entry.isDirectory) {
|
|
261
|
+
try {
|
|
262
|
+
const reader = entry.createReader();
|
|
263
|
+
const fileEntries = await new Promise((resolve, reject) => {
|
|
264
|
+
reader.readEntries(resolve, reject);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
for (const fileEntry of fileEntries) {
|
|
268
|
+
stack.push(fileEntry);
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.warn(`Failed to read directory ${entry.fullPath}:`, error);
|
|
272
|
+
}
|
|
273
|
+
} else if (entry.isFile) {
|
|
274
|
+
try {
|
|
275
|
+
const file = await new Promise((resolve, reject) => {
|
|
276
|
+
entry.file(resolve, reject);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const { ext, category } = categorizeFile(file.name);
|
|
280
|
+
const classification = category === FILE_CATEGORIES.PART
|
|
281
|
+
? classifyPart(entry.fullPath)
|
|
282
|
+
: null;
|
|
283
|
+
|
|
284
|
+
const buffer = await file.arrayBuffer();
|
|
285
|
+
|
|
286
|
+
filesMap.set(entry.fullPath, {
|
|
287
|
+
name: file.name,
|
|
288
|
+
ext,
|
|
289
|
+
size: file.size,
|
|
290
|
+
category,
|
|
291
|
+
classification,
|
|
292
|
+
buffer,
|
|
293
|
+
mimeType: file.type,
|
|
294
|
+
});
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.warn(`Failed to read file ${entry.fullPath}:`, error);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return filesMap;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Load an entire Inventor project from a directory handle
|
|
306
|
+
* @param {FileSystemDirectoryHandle} directoryHandle
|
|
307
|
+
* @returns {Promise<Object>} Project object with ipj, files, tree, stats
|
|
308
|
+
*/
|
|
309
|
+
export async function loadProject(directoryHandle) {
|
|
310
|
+
projectState.name = directoryHandle.name;
|
|
311
|
+
projectState.files.clear();
|
|
312
|
+
projectState.stats = {
|
|
313
|
+
parts: 0,
|
|
314
|
+
assemblies: 0,
|
|
315
|
+
drawings: 0,
|
|
316
|
+
total: 0,
|
|
317
|
+
byClassification: { custom: 0, standard: 0, buyout: 0 },
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Recursively read all files
|
|
321
|
+
for await (const entry of directoryHandle.values()) {
|
|
322
|
+
await traverseDirectory(entry, '');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Parse .ipj file if present
|
|
326
|
+
const ipjEntry = Array.from(projectState.files.values()).find(
|
|
327
|
+
(f) => f.ext === INVENTOR_EXTENSIONS.IPJ
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (ipjEntry && ipjEntry.buffer) {
|
|
331
|
+
projectState.ipj = parseIPJ(ipjEntry.buffer);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Build file tree
|
|
335
|
+
projectState.tree = buildFileTree(projectState.files);
|
|
336
|
+
|
|
337
|
+
// Calculate stats
|
|
338
|
+
updateStats();
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
name: projectState.name,
|
|
342
|
+
ipj: projectState.ipj,
|
|
343
|
+
files: projectState.files,
|
|
344
|
+
tree: projectState.tree,
|
|
345
|
+
stats: projectState.stats,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Recursively traverse directory structure
|
|
351
|
+
* @private
|
|
352
|
+
*/
|
|
353
|
+
async function traverseDirectory(entry, basePath) {
|
|
354
|
+
if (entry.kind === 'directory') {
|
|
355
|
+
const nextPath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
356
|
+
|
|
357
|
+
for await (const subEntry of entry.values()) {
|
|
358
|
+
await traverseDirectory(subEntry, nextPath);
|
|
359
|
+
}
|
|
360
|
+
} else if (entry.kind === 'file') {
|
|
361
|
+
const file = await entry.getFile();
|
|
362
|
+
const filePath = basePath ? `${basePath}/${file.name}` : file.name;
|
|
363
|
+
|
|
364
|
+
const { ext, category } = categorizeFile(file.name);
|
|
365
|
+
const classification = category === FILE_CATEGORIES.PART
|
|
366
|
+
? classifyPart(filePath)
|
|
367
|
+
: null;
|
|
368
|
+
|
|
369
|
+
const buffer = await file.arrayBuffer();
|
|
370
|
+
|
|
371
|
+
projectState.files.set(filePath, {
|
|
372
|
+
name: file.name,
|
|
373
|
+
ext,
|
|
374
|
+
size: file.size,
|
|
375
|
+
category,
|
|
376
|
+
classification,
|
|
377
|
+
buffer,
|
|
378
|
+
mimeType: file.type,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ============================================================================
|
|
384
|
+
// PROJECT STATISTICS
|
|
385
|
+
// ============================================================================
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Update project statistics from current file index
|
|
389
|
+
* @private
|
|
390
|
+
*/
|
|
391
|
+
function updateStats() {
|
|
392
|
+
projectState.stats = {
|
|
393
|
+
parts: 0,
|
|
394
|
+
assemblies: 0,
|
|
395
|
+
drawings: 0,
|
|
396
|
+
total: 0,
|
|
397
|
+
byClassification: { custom: 0, standard: 0, buyout: 0 },
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
for (const file of projectState.files.values()) {
|
|
401
|
+
projectState.stats.total++;
|
|
402
|
+
|
|
403
|
+
if (file.category === FILE_CATEGORIES.PART) {
|
|
404
|
+
projectState.stats.parts++;
|
|
405
|
+
if (file.classification) {
|
|
406
|
+
projectState.stats.byClassification[file.classification]++;
|
|
407
|
+
}
|
|
408
|
+
} else if (file.category === FILE_CATEGORIES.ASSEMBLY) {
|
|
409
|
+
projectState.stats.assemblies++;
|
|
410
|
+
} else if (file.category === FILE_CATEGORIES.DRAWING) {
|
|
411
|
+
projectState.stats.drawings++;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Get current project statistics
|
|
418
|
+
* @returns {Object}
|
|
419
|
+
*/
|
|
420
|
+
export function getProjectStats() {
|
|
421
|
+
return {
|
|
422
|
+
...projectState.stats,
|
|
423
|
+
name: projectState.name,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ============================================================================
|
|
428
|
+
// FILE RETRIEVAL
|
|
429
|
+
// ============================================================================
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Retrieve a file's ArrayBuffer by path
|
|
433
|
+
* @param {string} path
|
|
434
|
+
* @returns {ArrayBuffer|null}
|
|
435
|
+
*/
|
|
436
|
+
export function getFileByPath(path) {
|
|
437
|
+
const file = projectState.files.get(path);
|
|
438
|
+
return file ? file.buffer : null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get all files of a specific category
|
|
443
|
+
* @param {string} category - FILE_CATEGORIES.PART, etc.
|
|
444
|
+
* @returns {Array<{path, name, size, classification}>}
|
|
445
|
+
*/
|
|
446
|
+
export function getFilesByCategory(category) {
|
|
447
|
+
return Array.from(projectState.files.entries())
|
|
448
|
+
.filter(([, file]) => file.category === category)
|
|
449
|
+
.map(([path, file]) => ({
|
|
450
|
+
path,
|
|
451
|
+
name: file.name,
|
|
452
|
+
size: file.size,
|
|
453
|
+
classification: file.classification,
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ============================================================================
|
|
458
|
+
// FOLDER PICKER UI
|
|
459
|
+
// ============================================================================
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Show folder picker dialog and load project
|
|
463
|
+
* Uses File System Access API with fallback to webkitdirectory input
|
|
464
|
+
*
|
|
465
|
+
* @returns {Promise<Object>} Loaded project object or null if cancelled
|
|
466
|
+
*/
|
|
467
|
+
export async function showFolderPicker() {
|
|
468
|
+
try {
|
|
469
|
+
// Try modern File System Access API
|
|
470
|
+
if ('showDirectoryPicker' in window) {
|
|
471
|
+
const directoryHandle = await window.showDirectoryPicker();
|
|
472
|
+
return await loadProject(directoryHandle);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Fallback to webkitdirectory input
|
|
476
|
+
const input = document.createElement('input');
|
|
477
|
+
input.type = 'file';
|
|
478
|
+
input.webkitdirectory = true;
|
|
479
|
+
input.multiple = true;
|
|
480
|
+
|
|
481
|
+
return new Promise((resolve) => {
|
|
482
|
+
input.addEventListener('change', async () => {
|
|
483
|
+
if (input.files.length === 0) {
|
|
484
|
+
resolve(null);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const filesMap = new Map();
|
|
489
|
+
const commonRoot = findCommonRoot(input.files);
|
|
490
|
+
|
|
491
|
+
for (const file of input.files) {
|
|
492
|
+
const path = file.webkitRelativePath.replace(commonRoot, '').replace(/^\//, '');
|
|
493
|
+
|
|
494
|
+
if (!path) continue;
|
|
495
|
+
|
|
496
|
+
const { ext, category } = categorizeFile(file.name);
|
|
497
|
+
const classification = category === FILE_CATEGORIES.PART
|
|
498
|
+
? classifyPart(path)
|
|
499
|
+
: null;
|
|
500
|
+
|
|
501
|
+
const buffer = await file.arrayBuffer();
|
|
502
|
+
|
|
503
|
+
filesMap.set(path, {
|
|
504
|
+
name: file.name,
|
|
505
|
+
ext,
|
|
506
|
+
size: file.size,
|
|
507
|
+
category,
|
|
508
|
+
classification,
|
|
509
|
+
buffer,
|
|
510
|
+
mimeType: file.type,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
projectState.files = filesMap;
|
|
515
|
+
projectState.name = commonRoot.split('/').filter(Boolean)[0] || 'Project';
|
|
516
|
+
projectState.tree = buildFileTree(filesMap);
|
|
517
|
+
updateStats();
|
|
518
|
+
|
|
519
|
+
resolve({
|
|
520
|
+
name: projectState.name,
|
|
521
|
+
ipj: projectState.ipj,
|
|
522
|
+
files: projectState.files,
|
|
523
|
+
tree: projectState.tree,
|
|
524
|
+
stats: projectState.stats,
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
input.click();
|
|
529
|
+
});
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (error.name === 'AbortError') {
|
|
532
|
+
console.log('Folder picker cancelled by user');
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
console.error('Error showing folder picker:', error);
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Find common root directory from webkitRelativePath entries
|
|
542
|
+
* @private
|
|
543
|
+
*/
|
|
544
|
+
function findCommonRoot(files) {
|
|
545
|
+
if (files.length === 0) return '';
|
|
546
|
+
|
|
547
|
+
const paths = Array.from(files).map((f) => f.webkitRelativePath);
|
|
548
|
+
const firstPath = paths[0].split('/');
|
|
549
|
+
|
|
550
|
+
let commonDepth = 0;
|
|
551
|
+
for (let i = 0; i < firstPath.length; i++) {
|
|
552
|
+
if (paths.every((p) => p.split('/')[i] === firstPath[i])) {
|
|
553
|
+
commonDepth = i + 1;
|
|
554
|
+
} else {
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return firstPath.slice(0, commonDepth).join('/');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ============================================================================
|
|
563
|
+
// EXPORT SUMMARY
|
|
564
|
+
// ============================================================================
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Get current project state (mostly for debugging)
|
|
568
|
+
* @private
|
|
569
|
+
* @returns {Object}
|
|
570
|
+
*/
|
|
571
|
+
export function getProjectState() {
|
|
572
|
+
return {
|
|
573
|
+
name: projectState.name,
|
|
574
|
+
ipj: projectState.ipj,
|
|
575
|
+
fileCount: projectState.files.size,
|
|
576
|
+
stats: projectState.stats,
|
|
577
|
+
tree: projectState.tree,
|
|
578
|
+
};
|
|
579
|
+
}
|