@principal-ai/file-city-react 0.4.2 → 0.4.3

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,17 @@
1
+ import { CityData } from '@principal-ai/file-city-builder';
2
+ /**
3
+ * Generate a large number of file paths with realistic nested directory structures
4
+ */
5
+ export declare function generateLargeFilePaths(fileCount: number): string[];
6
+ /**
7
+ * Create CityData with a large number of files for stress testing subdirectory zoom
8
+ *
9
+ * @param fileCount - Number of files to generate (default: 8000)
10
+ * @param useCache - Whether to cache results (default: true)
11
+ */
12
+ export declare function createStressTestCityData(fileCount?: number, useCache?: boolean): CityData;
13
+ /**
14
+ * Clear the stress test cache (useful for testing memory)
15
+ */
16
+ export declare function clearStressTestCache(): void;
17
+ //# sourceMappingURL=stress-test-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stress-test-data.d.ts","sourceRoot":"","sources":["../../src/stories/stress-test-data.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EAGT,MAAM,iCAAiC,CAAC;AA2CzC;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAoElE;AAoBD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,GAAE,MAAa,EAAE,QAAQ,GAAE,OAAc,GAAG,QAAQ,CAwBrG;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C"}
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateLargeFilePaths = generateLargeFilePaths;
4
+ exports.createStressTestCityData = createStressTestCityData;
5
+ exports.clearStressTestCache = clearStressTestCache;
6
+ const file_city_builder_1 = require("@principal-ai/file-city-builder");
7
+ // Common file extensions for realistic distribution
8
+ const FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.css', '.json', '.md', '.test.ts', '.spec.tsx'];
9
+ // Top-level source directories
10
+ const TOP_LEVEL_DIRS = ['src', 'lib', 'packages', 'modules'];
11
+ // Second-level directories (domain areas)
12
+ const DOMAIN_DIRS = ['components', 'utils', 'services', 'hooks', 'types', 'helpers', 'core', 'api', 'features', 'pages'];
13
+ // Third-level directories (categories within domains)
14
+ const CATEGORY_DIRS = ['ui', 'forms', 'layout', 'navigation', 'data', 'auth', 'common', 'shared', 'internal', 'public'];
15
+ // Fourth-level directories (specific feature areas)
16
+ const FEATURE_DIRS = ['Button', 'Modal', 'Table', 'Card', 'Input', 'Select', 'Dialog', 'Menu', 'Tabs', 'List', 'Grid', 'Panel'];
17
+ // Fifth-level directories (variants/subfeatures)
18
+ const VARIANT_DIRS = ['variants', 'styles', 'hooks', 'utils', 'types', 'tests', '__tests__', 'stories', 'docs'];
19
+ // File name prefixes for realistic naming
20
+ const FILE_PREFIXES = ['index', 'main', 'handler', 'controller', 'service', 'helper', 'util', 'config', 'constants', 'types'];
21
+ /**
22
+ * Seeded random number generator for consistent results
23
+ */
24
+ function seededRandom(seed) {
25
+ return () => {
26
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff;
27
+ return seed / 0x7fffffff;
28
+ };
29
+ }
30
+ /**
31
+ * Generate a large number of file paths with realistic nested directory structures
32
+ */
33
+ function generateLargeFilePaths(fileCount) {
34
+ const files = [];
35
+ const random = seededRandom(42); // Consistent seed for reproducible results
36
+ for (let i = 0; i < fileCount; i++) {
37
+ const extension = FILE_EXTENSIONS[i % FILE_EXTENSIONS.length];
38
+ // Determine nesting depth (0-5 levels) with weighted distribution
39
+ // More files at medium depths (2-3), fewer at extremes
40
+ const depthRoll = random();
41
+ let depth;
42
+ if (depthRoll < 0.1)
43
+ depth = 1; // 10% at depth 1
44
+ else if (depthRoll < 0.25)
45
+ depth = 2; // 15% at depth 2
46
+ else if (depthRoll < 0.50)
47
+ depth = 3; // 25% at depth 3
48
+ else if (depthRoll < 0.75)
49
+ depth = 4; // 25% at depth 4
50
+ else if (depthRoll < 0.90)
51
+ depth = 5; // 15% at depth 5
52
+ else
53
+ depth = 6; // 10% at depth 6
54
+ const pathParts = [];
55
+ // Level 1: Top-level directory
56
+ // Weight 'src' heavily so it contains ~50% of files (for stress testing large subdirectory zoom)
57
+ const topLevelRoll = random();
58
+ if (topLevelRoll < 0.5) {
59
+ pathParts.push('src'); // 50% of files in src
60
+ }
61
+ else if (topLevelRoll < 0.7) {
62
+ pathParts.push('lib'); // 20% in lib
63
+ }
64
+ else if (topLevelRoll < 0.85) {
65
+ pathParts.push('packages'); // 15% in packages
66
+ }
67
+ else {
68
+ pathParts.push('modules'); // 15% in modules
69
+ }
70
+ // Level 2: Domain directory
71
+ if (depth >= 2) {
72
+ pathParts.push(DOMAIN_DIRS[Math.floor(random() * DOMAIN_DIRS.length)]);
73
+ }
74
+ // Level 3: Category directory
75
+ if (depth >= 3) {
76
+ pathParts.push(CATEGORY_DIRS[Math.floor(random() * CATEGORY_DIRS.length)]);
77
+ }
78
+ // Level 4: Feature directory
79
+ if (depth >= 4) {
80
+ pathParts.push(FEATURE_DIRS[Math.floor(random() * FEATURE_DIRS.length)]);
81
+ }
82
+ // Level 5: Variant directory
83
+ if (depth >= 5) {
84
+ pathParts.push(VARIANT_DIRS[Math.floor(random() * VARIANT_DIRS.length)]);
85
+ }
86
+ // Level 6: Additional nesting for very deep files
87
+ if (depth >= 6) {
88
+ pathParts.push(`nested${Math.floor(random() * 5)}`);
89
+ }
90
+ // Generate filename
91
+ const prefix = FILE_PREFIXES[Math.floor(random() * FILE_PREFIXES.length)];
92
+ const suffix = Math.floor(i / 10); // Group files by suffix number
93
+ const fileName = `${prefix}${suffix}${extension}`;
94
+ pathParts.push(fileName);
95
+ files.push(pathParts.join('/'));
96
+ }
97
+ return files;
98
+ }
99
+ /**
100
+ * Convert file paths to FileInfo objects
101
+ */
102
+ function createFileInfoList(paths) {
103
+ return paths.map(path => ({
104
+ name: path.split('/').pop() || path,
105
+ path: path,
106
+ relativePath: path,
107
+ size: 500 + Math.floor(Math.random() * 5000), // 500-5500 bytes
108
+ extension: path.includes('.') ? '.' + (path.split('.').pop() || '') : '',
109
+ lastModified: new Date(),
110
+ isDirectory: false,
111
+ }));
112
+ }
113
+ // Cache for stress test data to avoid regenerating
114
+ const stressTestCache = new Map();
115
+ /**
116
+ * Create CityData with a large number of files for stress testing subdirectory zoom
117
+ *
118
+ * @param fileCount - Number of files to generate (default: 8000)
119
+ * @param useCache - Whether to cache results (default: true)
120
+ */
121
+ function createStressTestCityData(fileCount = 8000, useCache = true) {
122
+ if (useCache && stressTestCache.has(fileCount)) {
123
+ return stressTestCache.get(fileCount);
124
+ }
125
+ const filePaths = generateLargeFilePaths(fileCount);
126
+ const fileInfos = createFileInfoList(filePaths);
127
+ const fileTree = (0, file_city_builder_1.buildFileSystemTreeFromFileInfoList)(fileInfos, `stress-test-${fileCount}`);
128
+ const builder = new file_city_builder_1.CodeCityBuilderWithGrid();
129
+ const cityData = builder.buildCityFromFileSystem(fileTree, '', {
130
+ paddingTop: 2,
131
+ paddingBottom: 2,
132
+ paddingLeft: 2,
133
+ paddingRight: 2,
134
+ paddingInner: 1,
135
+ paddingOuter: 3,
136
+ });
137
+ if (useCache) {
138
+ stressTestCache.set(fileCount, cityData);
139
+ }
140
+ return cityData;
141
+ }
142
+ /**
143
+ * Clear the stress test cache (useful for testing memory)
144
+ */
145
+ function clearStressTestCache() {
146
+ stressTestCache.clear();
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/file-city-react",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "React components for File City visualization",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,443 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+
4
+ import { ArchitectureMapHighlightLayers } from '../components/ArchitectureMapHighlightLayers';
5
+ import { HighlightLayer } from '../render/client/drawLayeredBuildings';
6
+ import { createStressTestCityData } from './stress-test-data';
7
+
8
+ // Wrapper component that regenerates cityData when fileCount changes
9
+ // and includes the click-to-zoom panel
10
+ function StressTestWrapper({
11
+ fileCount,
12
+ ...props
13
+ }: {
14
+ fileCount: number;
15
+ } & Omit<React.ComponentProps<typeof ArchitectureMapHighlightLayers>, 'cityData'>) {
16
+ const [zoomToPath, setZoomToPath] = useState<string | null>(null);
17
+ const [isAnimating, setIsAnimating] = useState(false);
18
+
19
+ const cityData = useMemo(() => {
20
+ console.log(`Generating city data for ${fileCount} files...`);
21
+ const start = performance.now();
22
+ const data = createStressTestCityData(fileCount, true);
23
+ const elapsed = performance.now() - start;
24
+ console.log(`Generated in ${elapsed.toFixed(2)}ms`);
25
+ return data;
26
+ }, [fileCount]);
27
+
28
+ // Get unique top-level directories for navigation buttons
29
+ const topLevelDirs = useMemo(() => {
30
+ return Array.from(
31
+ new Set(
32
+ cityData.districts
33
+ .map(d => d.path.split('/')[0])
34
+ .filter(Boolean),
35
+ ),
36
+ ).sort();
37
+ }, [cityData]);
38
+
39
+ // Get nested directories at different levels for navigation
40
+ const nestedDirs = useMemo(() => {
41
+ const allPaths = cityData.districts.map(d => d.path);
42
+
43
+ // Get depth-2 directories (e.g., src/components)
44
+ const depth2 = Array.from(
45
+ new Set(allPaths.filter(p => p.split('/').length === 2)),
46
+ ).sort().slice(0, 6);
47
+
48
+ // Get depth-3 directories (e.g., src/components/ui)
49
+ const depth3 = Array.from(
50
+ new Set(allPaths.filter(p => p.split('/').length === 3)),
51
+ ).sort().slice(0, 6);
52
+
53
+ // Get depth-4 directories (e.g., src/components/ui/Button)
54
+ const depth4 = Array.from(
55
+ new Set(allPaths.filter(p => p.split('/').length === 4)),
56
+ ).sort().slice(0, 4);
57
+
58
+ return { depth2, depth3, depth4 };
59
+ }, [cityData]);
60
+
61
+ const handleZoomTo = (path: string | null) => {
62
+ setIsAnimating(true);
63
+ setZoomToPath(path);
64
+ };
65
+
66
+ const handleZoomComplete = () => {
67
+ setIsAnimating(false);
68
+ };
69
+
70
+ // Create highlight layer for the focused directory
71
+ const highlightLayers: HighlightLayer[] = zoomToPath
72
+ ? [
73
+ {
74
+ id: 'zoom-focus',
75
+ name: 'Zoom Focus',
76
+ enabled: true,
77
+ color: '#3b82f6',
78
+ priority: 1,
79
+ items: [{ path: zoomToPath, type: 'directory' }],
80
+ },
81
+ ]
82
+ : [];
83
+
84
+ return (
85
+ <div style={{ position: 'relative', width: '100%', height: '100%' }}>
86
+ <ArchitectureMapHighlightLayers
87
+ cityData={cityData}
88
+ zoomToPath={zoomToPath}
89
+ onZoomComplete={handleZoomComplete}
90
+ zoomAnimationSpeed={0.1}
91
+ highlightLayers={highlightLayers}
92
+ onFileClick={(path, type) => {
93
+ if (type === 'directory') {
94
+ handleZoomTo(path);
95
+ }
96
+ }}
97
+ {...props}
98
+ />
99
+
100
+ {/* Navigation Controls */}
101
+ <div
102
+ style={{
103
+ position: 'absolute',
104
+ top: 20,
105
+ left: 20,
106
+ zIndex: 100,
107
+ display: 'flex',
108
+ flexDirection: 'column',
109
+ gap: '8px',
110
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
111
+ padding: '16px',
112
+ borderRadius: '8px',
113
+ maxWidth: '220px',
114
+ maxHeight: 'calc(100vh - 100px)',
115
+ overflowY: 'auto',
116
+ }}
117
+ >
118
+ <div
119
+ style={{
120
+ color: '#3b82f6',
121
+ fontFamily: 'monospace',
122
+ fontSize: '12px',
123
+ fontWeight: 'bold',
124
+ }}
125
+ >
126
+ Stress Test: {fileCount.toLocaleString()} files
127
+ </div>
128
+ <div
129
+ style={{
130
+ color: '#9ca3af',
131
+ fontFamily: 'monospace',
132
+ fontSize: '10px',
133
+ marginBottom: '8px',
134
+ }}
135
+ >
136
+ {isAnimating ? 'Animating...' : 'Click to zoom'}
137
+ </div>
138
+
139
+ {/* Reset button */}
140
+ <button
141
+ onClick={() => handleZoomTo(null)}
142
+ disabled={isAnimating}
143
+ style={{
144
+ padding: '8px 12px',
145
+ backgroundColor: zoomToPath === null ? '#3b82f6' : '#374151',
146
+ color: 'white',
147
+ border: 'none',
148
+ borderRadius: '4px',
149
+ cursor: isAnimating ? 'not-allowed' : 'pointer',
150
+ fontFamily: 'monospace',
151
+ fontSize: '12px',
152
+ opacity: isAnimating ? 0.6 : 1,
153
+ }}
154
+ >
155
+ Reset View
156
+ </button>
157
+
158
+ {/* Top-level directory buttons */}
159
+ {topLevelDirs.map(dir => (
160
+ <button
161
+ key={dir}
162
+ onClick={() => handleZoomTo(dir)}
163
+ disabled={isAnimating}
164
+ style={{
165
+ padding: '8px 12px',
166
+ backgroundColor: zoomToPath === dir ? '#3b82f6' : '#374151',
167
+ color: 'white',
168
+ border: 'none',
169
+ borderRadius: '4px',
170
+ cursor: isAnimating ? 'not-allowed' : 'pointer',
171
+ fontFamily: 'monospace',
172
+ fontSize: '12px',
173
+ textAlign: 'left',
174
+ opacity: isAnimating ? 0.6 : 1,
175
+ }}
176
+ >
177
+ {dir}
178
+ </button>
179
+ ))}
180
+
181
+ {/* Depth 2 directories (e.g., src/components) */}
182
+ {nestedDirs.depth2.length > 0 && (
183
+ <div
184
+ style={{
185
+ borderTop: '1px solid #4b5563',
186
+ paddingTop: '8px',
187
+ marginTop: '4px',
188
+ }}
189
+ >
190
+ <div
191
+ style={{
192
+ color: '#9ca3af',
193
+ fontFamily: 'monospace',
194
+ fontSize: '10px',
195
+ marginBottom: '4px',
196
+ }}
197
+ >
198
+ Depth 2:
199
+ </div>
200
+ {nestedDirs.depth2.map(dir => (
201
+ <button
202
+ key={dir}
203
+ onClick={() => handleZoomTo(dir)}
204
+ disabled={isAnimating}
205
+ style={{
206
+ padding: '6px 10px',
207
+ backgroundColor: zoomToPath === dir ? '#3b82f6' : '#1f2937',
208
+ color: 'white',
209
+ border: 'none',
210
+ borderRadius: '4px',
211
+ cursor: isAnimating ? 'not-allowed' : 'pointer',
212
+ fontFamily: 'monospace',
213
+ fontSize: '10px',
214
+ textAlign: 'left',
215
+ marginBottom: '4px',
216
+ width: '100%',
217
+ opacity: isAnimating ? 0.6 : 1,
218
+ overflow: 'hidden',
219
+ textOverflow: 'ellipsis',
220
+ whiteSpace: 'nowrap',
221
+ }}
222
+ title={dir}
223
+ >
224
+ {dir}
225
+ </button>
226
+ ))}
227
+ </div>
228
+ )}
229
+
230
+ {/* Depth 3 directories (e.g., src/components/ui) */}
231
+ {nestedDirs.depth3.length > 0 && (
232
+ <div
233
+ style={{
234
+ borderTop: '1px solid #4b5563',
235
+ paddingTop: '8px',
236
+ marginTop: '4px',
237
+ }}
238
+ >
239
+ <div
240
+ style={{
241
+ color: '#9ca3af',
242
+ fontFamily: 'monospace',
243
+ fontSize: '10px',
244
+ marginBottom: '4px',
245
+ }}
246
+ >
247
+ Depth 3:
248
+ </div>
249
+ {nestedDirs.depth3.map(dir => (
250
+ <button
251
+ key={dir}
252
+ onClick={() => handleZoomTo(dir)}
253
+ disabled={isAnimating}
254
+ style={{
255
+ padding: '6px 10px',
256
+ backgroundColor: zoomToPath === dir ? '#10b981' : '#1f2937',
257
+ color: 'white',
258
+ border: 'none',
259
+ borderRadius: '4px',
260
+ cursor: isAnimating ? 'not-allowed' : 'pointer',
261
+ fontFamily: 'monospace',
262
+ fontSize: '10px',
263
+ textAlign: 'left',
264
+ marginBottom: '4px',
265
+ width: '100%',
266
+ opacity: isAnimating ? 0.6 : 1,
267
+ overflow: 'hidden',
268
+ textOverflow: 'ellipsis',
269
+ whiteSpace: 'nowrap',
270
+ }}
271
+ title={dir}
272
+ >
273
+ {dir}
274
+ </button>
275
+ ))}
276
+ </div>
277
+ )}
278
+
279
+ {/* Depth 4 directories (e.g., src/components/ui/Button) */}
280
+ {nestedDirs.depth4.length > 0 && (
281
+ <div
282
+ style={{
283
+ borderTop: '1px solid #4b5563',
284
+ paddingTop: '8px',
285
+ marginTop: '4px',
286
+ }}
287
+ >
288
+ <div
289
+ style={{
290
+ color: '#9ca3af',
291
+ fontFamily: 'monospace',
292
+ fontSize: '10px',
293
+ marginBottom: '4px',
294
+ }}
295
+ >
296
+ Depth 4:
297
+ </div>
298
+ {nestedDirs.depth4.map(dir => (
299
+ <button
300
+ key={dir}
301
+ onClick={() => handleZoomTo(dir)}
302
+ disabled={isAnimating}
303
+ style={{
304
+ padding: '6px 10px',
305
+ backgroundColor: zoomToPath === dir ? '#f59e0b' : '#1f2937',
306
+ color: 'white',
307
+ border: 'none',
308
+ borderRadius: '4px',
309
+ cursor: isAnimating ? 'not-allowed' : 'pointer',
310
+ fontFamily: 'monospace',
311
+ fontSize: '10px',
312
+ textAlign: 'left',
313
+ marginBottom: '4px',
314
+ width: '100%',
315
+ opacity: isAnimating ? 0.6 : 1,
316
+ overflow: 'hidden',
317
+ textOverflow: 'ellipsis',
318
+ whiteSpace: 'nowrap',
319
+ }}
320
+ title={dir}
321
+ >
322
+ {dir}
323
+ </button>
324
+ ))}
325
+ </div>
326
+ )}
327
+ </div>
328
+
329
+ {/* Status info */}
330
+ <div
331
+ style={{
332
+ position: 'absolute',
333
+ bottom: 20,
334
+ left: 20,
335
+ zIndex: 100,
336
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
337
+ padding: '12px',
338
+ borderRadius: '8px',
339
+ color: 'white',
340
+ fontFamily: 'monospace',
341
+ fontSize: '11px',
342
+ }}
343
+ >
344
+ <div>Zoomed to: {zoomToPath || '(root)'}</div>
345
+ <div style={{ color: '#9ca3af', marginTop: '4px' }}>
346
+ Click directories in the map or use buttons above
347
+ </div>
348
+ </div>
349
+ </div>
350
+ );
351
+ }
352
+
353
+ const meta = {
354
+ title: 'Performance/Stress Test',
355
+ component: StressTestWrapper,
356
+ parameters: {
357
+ layout: 'fullscreen',
358
+ },
359
+ decorators: [
360
+ Story => (
361
+ <div style={{ width: '100vw', height: '100vh' }}>
362
+ <Story />
363
+ </div>
364
+ ),
365
+ ],
366
+ argTypes: {
367
+ fileCount: {
368
+ control: { type: 'number', min: 100, max: 20000, step: 100 },
369
+ description: 'Number of files to generate for stress testing',
370
+ },
371
+ },
372
+ } satisfies Meta<typeof StressTestWrapper>;
373
+
374
+ export default meta;
375
+ type Story = StoryObj<typeof meta>;
376
+
377
+ /**
378
+ * Test subdirectory zoom with a large number of files.
379
+ * Use the fileCount control to adjust the number of files.
380
+ *
381
+ * Try clicking on directories to zoom in and test performance.
382
+ */
383
+ export const SubdirectoryZoom: Story = {
384
+ args: {
385
+ fileCount: 8000,
386
+ enableZoom: true,
387
+ showGrid: true,
388
+ showFileNames: false,
389
+ fullSize: true,
390
+ },
391
+ };
392
+
393
+ /**
394
+ * Smaller stress test starting point (1000 files)
395
+ */
396
+ export const SmallStressTest: Story = {
397
+ args: {
398
+ fileCount: 1000,
399
+ enableZoom: true,
400
+ showGrid: true,
401
+ showFileNames: false,
402
+ fullSize: true,
403
+ },
404
+ };
405
+
406
+ /**
407
+ * Medium stress test (5000 files)
408
+ */
409
+ export const MediumStressTest: Story = {
410
+ args: {
411
+ fileCount: 5000,
412
+ enableZoom: true,
413
+ showGrid: true,
414
+ showFileNames: false,
415
+ fullSize: true,
416
+ },
417
+ };
418
+
419
+ /**
420
+ * Large stress test (10000 files)
421
+ */
422
+ export const LargeStressTest: Story = {
423
+ args: {
424
+ fileCount: 10000,
425
+ enableZoom: true,
426
+ showGrid: true,
427
+ showFileNames: false,
428
+ fullSize: true,
429
+ },
430
+ };
431
+
432
+ /**
433
+ * Extreme stress test (20000 files) - may be slow!
434
+ */
435
+ export const ExtremeStressTest: Story = {
436
+ args: {
437
+ fileCount: 20000,
438
+ enableZoom: true,
439
+ showGrid: true,
440
+ showFileNames: false,
441
+ fullSize: true,
442
+ },
443
+ };
@@ -0,0 +1,176 @@
1
+ import {
2
+ CityData,
3
+ CodeCityBuilderWithGrid,
4
+ buildFileSystemTreeFromFileInfoList,
5
+ } from '@principal-ai/file-city-builder';
6
+
7
+ interface FileInfo {
8
+ name: string;
9
+ path: string;
10
+ relativePath: string;
11
+ size: number;
12
+ extension: string;
13
+ lastModified: Date;
14
+ isDirectory: boolean;
15
+ }
16
+
17
+ // Common file extensions for realistic distribution
18
+ const FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.css', '.json', '.md', '.test.ts', '.spec.tsx'];
19
+
20
+ // Top-level source directories
21
+ const TOP_LEVEL_DIRS = ['src', 'lib', 'packages', 'modules'];
22
+
23
+ // Second-level directories (domain areas)
24
+ const DOMAIN_DIRS = ['components', 'utils', 'services', 'hooks', 'types', 'helpers', 'core', 'api', 'features', 'pages'];
25
+
26
+ // Third-level directories (categories within domains)
27
+ const CATEGORY_DIRS = ['ui', 'forms', 'layout', 'navigation', 'data', 'auth', 'common', 'shared', 'internal', 'public'];
28
+
29
+ // Fourth-level directories (specific feature areas)
30
+ const FEATURE_DIRS = ['Button', 'Modal', 'Table', 'Card', 'Input', 'Select', 'Dialog', 'Menu', 'Tabs', 'List', 'Grid', 'Panel'];
31
+
32
+ // Fifth-level directories (variants/subfeatures)
33
+ const VARIANT_DIRS = ['variants', 'styles', 'hooks', 'utils', 'types', 'tests', '__tests__', 'stories', 'docs'];
34
+
35
+ // File name prefixes for realistic naming
36
+ const FILE_PREFIXES = ['index', 'main', 'handler', 'controller', 'service', 'helper', 'util', 'config', 'constants', 'types'];
37
+
38
+ /**
39
+ * Seeded random number generator for consistent results
40
+ */
41
+ function seededRandom(seed: number): () => number {
42
+ return () => {
43
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff;
44
+ return seed / 0x7fffffff;
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Generate a large number of file paths with realistic nested directory structures
50
+ */
51
+ export function generateLargeFilePaths(fileCount: number): string[] {
52
+ const files: string[] = [];
53
+ const random = seededRandom(42); // Consistent seed for reproducible results
54
+
55
+ for (let i = 0; i < fileCount; i++) {
56
+ const extension = FILE_EXTENSIONS[i % FILE_EXTENSIONS.length];
57
+
58
+ // Determine nesting depth (0-5 levels) with weighted distribution
59
+ // More files at medium depths (2-3), fewer at extremes
60
+ const depthRoll = random();
61
+ let depth: number;
62
+ if (depthRoll < 0.1) depth = 1; // 10% at depth 1
63
+ else if (depthRoll < 0.25) depth = 2; // 15% at depth 2
64
+ else if (depthRoll < 0.50) depth = 3; // 25% at depth 3
65
+ else if (depthRoll < 0.75) depth = 4; // 25% at depth 4
66
+ else if (depthRoll < 0.90) depth = 5; // 15% at depth 5
67
+ else depth = 6; // 10% at depth 6
68
+
69
+ const pathParts: string[] = [];
70
+
71
+ // Level 1: Top-level directory
72
+ // Weight 'src' heavily so it contains ~50% of files (for stress testing large subdirectory zoom)
73
+ const topLevelRoll = random();
74
+ if (topLevelRoll < 0.5) {
75
+ pathParts.push('src'); // 50% of files in src
76
+ } else if (topLevelRoll < 0.7) {
77
+ pathParts.push('lib'); // 20% in lib
78
+ } else if (topLevelRoll < 0.85) {
79
+ pathParts.push('packages'); // 15% in packages
80
+ } else {
81
+ pathParts.push('modules'); // 15% in modules
82
+ }
83
+
84
+ // Level 2: Domain directory
85
+ if (depth >= 2) {
86
+ pathParts.push(DOMAIN_DIRS[Math.floor(random() * DOMAIN_DIRS.length)]);
87
+ }
88
+
89
+ // Level 3: Category directory
90
+ if (depth >= 3) {
91
+ pathParts.push(CATEGORY_DIRS[Math.floor(random() * CATEGORY_DIRS.length)]);
92
+ }
93
+
94
+ // Level 4: Feature directory
95
+ if (depth >= 4) {
96
+ pathParts.push(FEATURE_DIRS[Math.floor(random() * FEATURE_DIRS.length)]);
97
+ }
98
+
99
+ // Level 5: Variant directory
100
+ if (depth >= 5) {
101
+ pathParts.push(VARIANT_DIRS[Math.floor(random() * VARIANT_DIRS.length)]);
102
+ }
103
+
104
+ // Level 6: Additional nesting for very deep files
105
+ if (depth >= 6) {
106
+ pathParts.push(`nested${Math.floor(random() * 5)}`);
107
+ }
108
+
109
+ // Generate filename
110
+ const prefix = FILE_PREFIXES[Math.floor(random() * FILE_PREFIXES.length)];
111
+ const suffix = Math.floor(i / 10); // Group files by suffix number
112
+ const fileName = `${prefix}${suffix}${extension}`;
113
+
114
+ pathParts.push(fileName);
115
+ files.push(pathParts.join('/'));
116
+ }
117
+
118
+ return files;
119
+ }
120
+
121
+ /**
122
+ * Convert file paths to FileInfo objects
123
+ */
124
+ function createFileInfoList(paths: string[]): FileInfo[] {
125
+ return paths.map(path => ({
126
+ name: path.split('/').pop() || path,
127
+ path: path,
128
+ relativePath: path,
129
+ size: 500 + Math.floor(Math.random() * 5000), // 500-5500 bytes
130
+ extension: path.includes('.') ? '.' + (path.split('.').pop() || '') : '',
131
+ lastModified: new Date(),
132
+ isDirectory: false,
133
+ }));
134
+ }
135
+
136
+ // Cache for stress test data to avoid regenerating
137
+ const stressTestCache = new Map<number, CityData>();
138
+
139
+ /**
140
+ * Create CityData with a large number of files for stress testing subdirectory zoom
141
+ *
142
+ * @param fileCount - Number of files to generate (default: 8000)
143
+ * @param useCache - Whether to cache results (default: true)
144
+ */
145
+ export function createStressTestCityData(fileCount: number = 8000, useCache: boolean = true): CityData {
146
+ if (useCache && stressTestCache.has(fileCount)) {
147
+ return stressTestCache.get(fileCount)!;
148
+ }
149
+
150
+ const filePaths = generateLargeFilePaths(fileCount);
151
+ const fileInfos = createFileInfoList(filePaths);
152
+ const fileTree = buildFileSystemTreeFromFileInfoList(fileInfos as any, `stress-test-${fileCount}`);
153
+
154
+ const builder = new CodeCityBuilderWithGrid();
155
+ const cityData = builder.buildCityFromFileSystem(fileTree, '', {
156
+ paddingTop: 2,
157
+ paddingBottom: 2,
158
+ paddingLeft: 2,
159
+ paddingRight: 2,
160
+ paddingInner: 1,
161
+ paddingOuter: 3,
162
+ });
163
+
164
+ if (useCache) {
165
+ stressTestCache.set(fileCount, cityData);
166
+ }
167
+
168
+ return cityData;
169
+ }
170
+
171
+ /**
172
+ * Clear the stress test cache (useful for testing memory)
173
+ */
174
+ export function clearStressTestCache(): void {
175
+ stressTestCache.clear();
176
+ }