@principal-ai/file-city-react 0.4.2 → 0.4.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.
@@ -1 +1 @@
1
- {"version":3,"file":"ArchitectureMapHighlightLayers.d.ts","sourceRoot":"","sources":["../../src/components/ArchitectureMapHighlightLayers.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ArchitectureMapHighlightLayers.d.ts","sourceRoot":"","sources":["../../src/components/ArchitectureMapHighlightLayers.tsx"],"names":[],"mappings":"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"}
@@ -696,6 +696,8 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
696
696
  }
697
697
  return layers;
698
698
  }, [stableLayers, dynamicLayers, abstractionLayer]);
699
+ // Memoize layer index for O(1) path lookups - only rebuilds when layers change
700
+ const layerIndex = (0, react_1.useMemo)(() => new drawLayeredBuildings_1.LayerIndex(allLayers), [allLayers]);
699
701
  // Memoize abstracted paths lookup - only recalculates when abstraction layer changes
700
702
  const { abstractedPathsSet, abstractedPathLookup } = (0, react_1.useMemo)(() => {
701
703
  const pathsSet = new Set();
@@ -828,9 +830,9 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
828
830
  // Use memoized visible districts and buildings (pre-filtered, doesn't recalculate on hover)
829
831
  // Draw districts with layer support
830
832
  (0, drawLayeredBuildings_1.drawLayeredDistricts)(ctx, visibleDistrictsMemo, worldToCanvas, scale * zoomState.scale, allLayers, interactionState.hoveredDistrict, fullSize, resolvedDefaultDirectoryColor, filteredCityData.metadata.layoutConfig, abstractedPathsSet, // Pass abstracted paths to skip labels
831
- showDirectoryLabels, districtBorderRadius);
833
+ showDirectoryLabels, districtBorderRadius, layerIndex);
832
834
  // Draw buildings with layer support
833
- (0, drawLayeredBuildings_1.drawLayeredBuildings)(ctx, visibleBuildingsMemo, worldToCanvas, scale * zoomState.scale, allLayers, interactionState.hoveredBuilding, resolvedDefaultBuildingColor, showFileNames, resolvedHoverBorderColor, disableOpacityDimming, showFileTypeIcons, buildingBorderRadius);
835
+ (0, drawLayeredBuildings_1.drawLayeredBuildings)(ctx, visibleBuildingsMemo, worldToCanvas, scale * zoomState.scale, allLayers, interactionState.hoveredBuilding, resolvedDefaultBuildingColor, showFileNames, resolvedHoverBorderColor, disableOpacityDimming, showFileTypeIcons, buildingBorderRadius, layerIndex);
834
836
  // Performance monitoring end available for debugging
835
837
  // Performance stats available but not logged to reduce console noise
836
838
  // Uncomment for debugging: render time, buildings/districts counts, layer counts
@@ -863,6 +865,7 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
863
865
  visibleDistrictsMemo,
864
866
  visibleBuildingsMemo,
865
867
  abstractedPathsSet,
868
+ layerIndex,
866
869
  ]);
867
870
  // Optimized hit testing
868
871
  const performHitTest = (0, react_1.useCallback)((canvasX, canvasY) => {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { ArchitectureMapHighlightLayers, type ArchitectureMapHighlightLayersProps, } from './components/ArchitectureMapHighlightLayers';
2
- export { type LayerRenderStrategy, type LayerItem, type HighlightLayer, } from './render/client/drawLayeredBuildings';
2
+ export { type LayerRenderStrategy, type LayerItem, type HighlightLayer, LayerIndex, } from './render/client/drawLayeredBuildings';
3
3
  export { type MapInteractionState, type MapDisplayOptions } from './types/react-types';
4
4
  export { filterCityDataForSelectiveRender, filterCityDataForSubdirectory, filterCityDataForMultipleDirectories, } from './builder/cityDataUtils';
5
5
  export { createFileColorHighlightLayers, getDefaultFileColorConfig, getFileColorMapping, } from './utils/fileColorHighlightLayers';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,KAAK,mCAAmC,GACzC,MAAM,6CAA6C,CAAC;AAGrD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,cAAc,GACpB,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EACL,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,kCAAkC,CAAC;AAG1C,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,QAAQ,EACR,UAAU,GACX,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAG7F,YAAY,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAGhE,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,KAAK,mCAAmC,GACzC,MAAM,6CAA6C,CAAC;AAGrD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,UAAU,GACX,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EACL,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,kCAAkC,CAAC;AAG1C,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,QAAQ,EACR,UAAU,GACX,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAG7F,YAAY,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAGhE,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC"}
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useTheme = exports.ThemeProvider = exports.CityViewWithReactFlow = exports.useCodeCityData = exports.MultiVersionCityBuilder = exports.getFileColorMapping = exports.getDefaultFileColorConfig = exports.createFileColorHighlightLayers = exports.filterCityDataForMultipleDirectories = exports.filterCityDataForSubdirectory = exports.filterCityDataForSelectiveRender = exports.ArchitectureMapHighlightLayers = void 0;
3
+ exports.useTheme = exports.ThemeProvider = exports.CityViewWithReactFlow = exports.useCodeCityData = exports.MultiVersionCityBuilder = exports.getFileColorMapping = exports.getDefaultFileColorConfig = exports.createFileColorHighlightLayers = exports.filterCityDataForMultipleDirectories = exports.filterCityDataForSubdirectory = exports.filterCityDataForSelectiveRender = exports.LayerIndex = exports.ArchitectureMapHighlightLayers = void 0;
4
4
  // Main component export
5
5
  var ArchitectureMapHighlightLayers_1 = require("./components/ArchitectureMapHighlightLayers");
6
6
  Object.defineProperty(exports, "ArchitectureMapHighlightLayers", { enumerable: true, get: function () { return ArchitectureMapHighlightLayers_1.ArchitectureMapHighlightLayers; } });
7
+ // Layer and rendering types
8
+ var drawLayeredBuildings_1 = require("./render/client/drawLayeredBuildings");
9
+ Object.defineProperty(exports, "LayerIndex", { enumerable: true, get: function () { return drawLayeredBuildings_1.LayerIndex; } });
7
10
  // Utility functions
8
11
  var cityDataUtils_1 = require("./builder/cityDataUtils");
9
12
  Object.defineProperty(exports, "filterCityDataForSelectiveRender", { enumerable: true, get: function () { return cityDataUtils_1.filterCityDataForSelectiveRender; } });
@@ -32,6 +32,26 @@ export interface HighlightLayer {
32
32
  items: LayerItem[];
33
33
  dynamic?: boolean;
34
34
  }
35
+ /**
36
+ * LayerIndex provides O(1) path lookups instead of O(n) iteration.
37
+ * This dramatically improves performance with large numbers of layer items.
38
+ */
39
+ export declare class LayerIndex {
40
+ private exactIndex;
41
+ private directoryPaths;
42
+ private sortedCache;
43
+ constructor(layers: HighlightLayer[]);
44
+ private buildIndex;
45
+ /**
46
+ * Get all layer items that apply to a given path.
47
+ * For 'exact' mode: only matches the exact path.
48
+ * For 'children' mode: matches exact path OR parent directories that contain this path.
49
+ */
50
+ getItemsForPath(path: string, checkType?: 'exact' | 'children'): Array<{
51
+ layer: HighlightLayer;
52
+ item: LayerItem;
53
+ }>;
54
+ }
35
55
  export declare function drawGrid(ctx: CanvasRenderingContext2D, width: number, height: number, gridSize: number): void;
36
56
  export declare function drawLayeredDistricts(ctx: CanvasRenderingContext2D, districts: CityDistrict[], worldToCanvas: (x: number, z: number) => {
37
57
  x: number;
@@ -43,9 +63,11 @@ layers: HighlightLayer[], hoveredDistrict?: CityDistrict | null, fullSize?: bool
43
63
  paddingLeft: number;
44
64
  paddingRight: number;
45
65
  }, abstractedPaths?: Set<string>, // Paths of directories that are abstracted (have covers)
46
- showDirectoryLabels?: boolean, borderRadius?: number): void;
66
+ showDirectoryLabels?: boolean, borderRadius?: number, // Border radius for districts (default: sharp corners)
67
+ layerIndex?: LayerIndex): void;
47
68
  export declare function drawLayeredBuildings(ctx: CanvasRenderingContext2D, buildings: CityBuilding[], worldToCanvas: (x: number, z: number) => {
48
69
  x: number;
49
70
  y: number;
50
- }, scale: number, layers: HighlightLayer[], hoveredBuilding?: CityBuilding | null, defaultBuildingColor?: string, showFileNames?: boolean, hoverBorderColor?: string, disableOpacityDimming?: boolean, showFileTypeIcons?: boolean, borderRadius?: number): void;
71
+ }, scale: number, layers: HighlightLayer[], hoveredBuilding?: CityBuilding | null, defaultBuildingColor?: string, showFileNames?: boolean, hoverBorderColor?: string, disableOpacityDimming?: boolean, showFileTypeIcons?: boolean, borderRadius?: number, // Border radius for buildings (default: 0 - sharp corners)
72
+ layerIndex?: LayerIndex): void;
51
73
  //# sourceMappingURL=drawLayeredBuildings.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"drawLayeredBuildings.d.ts","sourceRoot":"","sources":["../../../src/render/client/drawLayeredBuildings.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"drawLayeredBuildings.d.ts","sourceRoot":"","sources":["../../../src/render/client/drawLayeredBuildings.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LayerIndex = void 0;
3
4
  exports.drawGrid = drawGrid;
4
5
  exports.drawLayeredDistricts = drawLayeredDistricts;
5
6
  exports.drawLayeredBuildings = drawLayeredBuildings;
@@ -20,6 +21,87 @@ function pathMatchesItem(path, item, checkType = 'children') {
20
21
  }
21
22
  }
22
23
  }
24
+ /**
25
+ * LayerIndex provides O(1) path lookups instead of O(n) iteration.
26
+ * This dramatically improves performance with large numbers of layer items.
27
+ */
28
+ class LayerIndex {
29
+ constructor(layers) {
30
+ // Map from exact path to layer items (for file items)
31
+ this.exactIndex = new Map();
32
+ // Sorted list of directory paths for prefix matching
33
+ this.directoryPaths = [];
34
+ // Cache for sorted results by priority
35
+ this.sortedCache = new Map();
36
+ this.buildIndex(layers);
37
+ }
38
+ buildIndex(layers) {
39
+ for (const layer of layers) {
40
+ if (!layer.enabled)
41
+ continue;
42
+ for (const item of layer.items) {
43
+ const entry = { layer, item };
44
+ if (item.type === 'file') {
45
+ // File items: exact match only
46
+ const existing = this.exactIndex.get(item.path);
47
+ if (existing) {
48
+ existing.push(entry);
49
+ }
50
+ else {
51
+ this.exactIndex.set(item.path, [entry]);
52
+ }
53
+ }
54
+ else {
55
+ // Directory items: need prefix matching
56
+ this.directoryPaths.push({ path: item.path, layer, item });
57
+ }
58
+ }
59
+ }
60
+ // Sort directory paths by length (longest first) for correct matching
61
+ this.directoryPaths.sort((a, b) => b.path.length - a.path.length);
62
+ }
63
+ /**
64
+ * Get all layer items that apply to a given path.
65
+ * For 'exact' mode: only matches the exact path.
66
+ * For 'children' mode: matches exact path OR parent directories that contain this path.
67
+ */
68
+ getItemsForPath(path, checkType = 'children') {
69
+ // Check cache first
70
+ const cacheKey = `${path}:${checkType}`;
71
+ const cached = this.sortedCache.get(cacheKey);
72
+ if (cached)
73
+ return cached;
74
+ const matches = [];
75
+ // Check exact matches (file items)
76
+ const exactMatches = this.exactIndex.get(path);
77
+ if (exactMatches) {
78
+ matches.push(...exactMatches);
79
+ }
80
+ // Check directory matches
81
+ if (checkType === 'exact') {
82
+ // For exact mode, only match directories with exact path
83
+ for (const dir of this.directoryPaths) {
84
+ if (dir.path === path) {
85
+ matches.push({ layer: dir.layer, item: dir.item });
86
+ }
87
+ }
88
+ }
89
+ else {
90
+ // For children mode, check if path is inside any directory
91
+ for (const dir of this.directoryPaths) {
92
+ if (path === dir.path || path.startsWith(dir.path + '/')) {
93
+ matches.push({ layer: dir.layer, item: dir.item });
94
+ }
95
+ }
96
+ }
97
+ // Sort by priority (highest first)
98
+ const sorted = matches.sort((a, b) => b.layer.priority - a.layer.priority);
99
+ // Cache the result
100
+ this.sortedCache.set(cacheKey, sorted);
101
+ return sorted;
102
+ }
103
+ }
104
+ exports.LayerIndex = LayerIndex;
23
105
  // Helper function to draw rounded rectangles
24
106
  function drawRoundedRect(ctx, x, y, width, height, radius, fill, stroke) {
25
107
  ctx.beginPath();
@@ -339,12 +421,15 @@ function applyLayerRendering(ctx, bounds, layer, item, scale, borderRadius = 0)
339
421
  // Draw districts with layer support
340
422
  function drawLayeredDistricts(ctx, districts, worldToCanvas, scale, // This includes the zoom scale for text proportionality
341
423
  layers, hoveredDistrict, fullSize, defaultDirectoryColor, layoutConfig, abstractedPaths, // Paths of directories that are abstracted (have covers)
342
- showDirectoryLabels = true, borderRadius = 0) {
424
+ showDirectoryLabels = true, borderRadius = 0, // Border radius for districts (default: sharp corners)
425
+ layerIndex) {
426
+ // Build index once for all districts (O(n) instead of O(n²))
427
+ const index = layerIndex || new LayerIndex(layers);
343
428
  districts.forEach(district => {
344
429
  const districtPath = district.path || '';
345
430
  const isRoot = !districtPath || districtPath === '';
346
431
  // Check if this root district has layer matches (like covers) - if so, render it
347
- const rootLayerMatches = getLayerItemsForPath(districtPath, layers, 'exact');
432
+ const rootLayerMatches = index.getItemsForPath(districtPath, 'exact');
348
433
  const hasLayerRendering = rootLayerMatches.length > 0;
349
434
  // Skip root districts unless they have layer rendering (covers, highlights, etc.)
350
435
  if (isRoot && !hasLayerRendering)
@@ -371,7 +456,7 @@ showDirectoryLabels = true, borderRadius = 0) {
371
456
  let opacity = 0.3;
372
457
  let borderOpacity = 0.6;
373
458
  // Check if district has layer highlighting - use exact matching for districts
374
- const layerMatches = getLayerItemsForPath(districtPath, layers, 'exact');
459
+ const layerMatches = index.getItemsForPath(districtPath, 'exact');
375
460
  const hasLayerHighlight = layerMatches.length > 0;
376
461
  if (hasLayerHighlight) {
377
462
  opacity = 0.5;
@@ -551,7 +636,10 @@ function isReactFile(fileExtension) {
551
636
  return ext === '.jsx' || ext === '.tsx';
552
637
  }
553
638
  // Draw buildings with layer support
554
- function drawLayeredBuildings(ctx, buildings, worldToCanvas, scale, layers, hoveredBuilding, defaultBuildingColor, showFileNames, hoverBorderColor, disableOpacityDimming, showFileTypeIcons, borderRadius = 0) {
639
+ function drawLayeredBuildings(ctx, buildings, worldToCanvas, scale, layers, hoveredBuilding, defaultBuildingColor, showFileNames, hoverBorderColor, disableOpacityDimming, showFileTypeIcons, borderRadius = 0, // Border radius for buildings (default: 0 - sharp corners)
640
+ layerIndex) {
641
+ // Build index once for all buildings (O(n) instead of O(n²))
642
+ const index = layerIndex || new LayerIndex(layers);
555
643
  buildings.forEach(building => {
556
644
  const pos = worldToCanvas(building.position.x, building.position.z);
557
645
  // Calculate building dimensions
@@ -572,7 +660,7 @@ function drawLayeredBuildings(ctx, buildings, worldToCanvas, scale, layers, hove
572
660
  height: height,
573
661
  };
574
662
  // Get layer matches for this building - only check file items, not parent directories
575
- const layerMatches = getLayerItemsForPath(building.path, layers).filter(match => match.item.type === 'file'); // Only apply file-specific highlights to buildings
663
+ const layerMatches = index.getItemsForPath(building.path).filter(match => match.item.type === 'file'); // Only apply file-specific highlights to buildings
576
664
  const hasLayerHighlight = layerMatches.length > 0;
577
665
  const isHovered = hoveredBuilding === building;
578
666
  // Building color
@@ -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.4",
4
4
  "description": "React components for File City visualization",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,6 +12,7 @@ import {
12
12
  drawGrid,
13
13
  HighlightLayer,
14
14
  LayerItem,
15
+ LayerIndex,
15
16
  } from '../render/client/drawLayeredBuildings';
16
17
  import {
17
18
  CityData,
@@ -1007,6 +1008,9 @@ function ArchitectureMapHighlightLayersInner({
1007
1008
  return layers;
1008
1009
  }, [stableLayers, dynamicLayers, abstractionLayer]);
1009
1010
 
1011
+ // Memoize layer index for O(1) path lookups - only rebuilds when layers change
1012
+ const layerIndex = useMemo(() => new LayerIndex(allLayers), [allLayers]);
1013
+
1010
1014
  // Memoize abstracted paths lookup - only recalculates when abstraction layer changes
1011
1015
  const { abstractedPathsSet, abstractedPathLookup } = useMemo(() => {
1012
1016
  const pathsSet = new Set<string>();
@@ -1189,6 +1193,7 @@ function ArchitectureMapHighlightLayersInner({
1189
1193
  abstractedPathsSet, // Pass abstracted paths to skip labels
1190
1194
  showDirectoryLabels,
1191
1195
  districtBorderRadius,
1196
+ layerIndex, // Pre-built index for O(1) lookups
1192
1197
  );
1193
1198
 
1194
1199
  // Draw buildings with layer support
@@ -1205,6 +1210,7 @@ function ArchitectureMapHighlightLayersInner({
1205
1210
  disableOpacityDimming,
1206
1211
  showFileTypeIcons,
1207
1212
  buildingBorderRadius,
1213
+ layerIndex, // Pre-built index for O(1) lookups
1208
1214
  );
1209
1215
 
1210
1216
  // Performance monitoring end available for debugging
@@ -1239,6 +1245,7 @@ function ArchitectureMapHighlightLayersInner({
1239
1245
  visibleDistrictsMemo,
1240
1246
  visibleBuildingsMemo,
1241
1247
  abstractedPathsSet,
1248
+ layerIndex,
1242
1249
  ]);
1243
1250
 
1244
1251
  // Optimized hit testing
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export {
9
9
  type LayerRenderStrategy,
10
10
  type LayerItem,
11
11
  type HighlightLayer,
12
+ LayerIndex,
12
13
  } from './render/client/drawLayeredBuildings';
13
14
 
14
15
  // React-specific types
@@ -66,6 +66,97 @@ function pathMatchesItem(
66
66
  }
67
67
  }
68
68
 
69
+ /**
70
+ * LayerIndex provides O(1) path lookups instead of O(n) iteration.
71
+ * This dramatically improves performance with large numbers of layer items.
72
+ */
73
+ export class LayerIndex {
74
+ // Map from exact path to layer items (for file items)
75
+ private exactIndex: Map<string, Array<{ layer: HighlightLayer; item: LayerItem }>> = new Map();
76
+ // Sorted list of directory paths for prefix matching
77
+ private directoryPaths: Array<{ path: string; layer: HighlightLayer; item: LayerItem }> = [];
78
+ // Cache for sorted results by priority
79
+ private sortedCache: Map<string, Array<{ layer: HighlightLayer; item: LayerItem }>> = new Map();
80
+
81
+ constructor(layers: HighlightLayer[]) {
82
+ this.buildIndex(layers);
83
+ }
84
+
85
+ private buildIndex(layers: HighlightLayer[]) {
86
+ for (const layer of layers) {
87
+ if (!layer.enabled) continue;
88
+
89
+ for (const item of layer.items) {
90
+ const entry = { layer, item };
91
+
92
+ if (item.type === 'file') {
93
+ // File items: exact match only
94
+ const existing = this.exactIndex.get(item.path);
95
+ if (existing) {
96
+ existing.push(entry);
97
+ } else {
98
+ this.exactIndex.set(item.path, [entry]);
99
+ }
100
+ } else {
101
+ // Directory items: need prefix matching
102
+ this.directoryPaths.push({ path: item.path, layer, item });
103
+ }
104
+ }
105
+ }
106
+
107
+ // Sort directory paths by length (longest first) for correct matching
108
+ this.directoryPaths.sort((a, b) => b.path.length - a.path.length);
109
+ }
110
+
111
+ /**
112
+ * Get all layer items that apply to a given path.
113
+ * For 'exact' mode: only matches the exact path.
114
+ * For 'children' mode: matches exact path OR parent directories that contain this path.
115
+ */
116
+ getItemsForPath(
117
+ path: string,
118
+ checkType: 'exact' | 'children' = 'children',
119
+ ): Array<{ layer: HighlightLayer; item: LayerItem }> {
120
+ // Check cache first
121
+ const cacheKey = `${path}:${checkType}`;
122
+ const cached = this.sortedCache.get(cacheKey);
123
+ if (cached) return cached;
124
+
125
+ const matches: Array<{ layer: HighlightLayer; item: LayerItem }> = [];
126
+
127
+ // Check exact matches (file items)
128
+ const exactMatches = this.exactIndex.get(path);
129
+ if (exactMatches) {
130
+ matches.push(...exactMatches);
131
+ }
132
+
133
+ // Check directory matches
134
+ if (checkType === 'exact') {
135
+ // For exact mode, only match directories with exact path
136
+ for (const dir of this.directoryPaths) {
137
+ if (dir.path === path) {
138
+ matches.push({ layer: dir.layer, item: dir.item });
139
+ }
140
+ }
141
+ } else {
142
+ // For children mode, check if path is inside any directory
143
+ for (const dir of this.directoryPaths) {
144
+ if (path === dir.path || path.startsWith(dir.path + '/')) {
145
+ matches.push({ layer: dir.layer, item: dir.item });
146
+ }
147
+ }
148
+ }
149
+
150
+ // Sort by priority (highest first)
151
+ const sorted = matches.sort((a, b) => b.layer.priority - a.layer.priority);
152
+
153
+ // Cache the result
154
+ this.sortedCache.set(cacheKey, sorted);
155
+
156
+ return sorted;
157
+ }
158
+ }
159
+
69
160
  // Helper function to draw rounded rectangles
70
161
  function drawRoundedRect(
71
162
  ctx: CanvasRenderingContext2D,
@@ -496,13 +587,17 @@ export function drawLayeredDistricts(
496
587
  abstractedPaths?: Set<string>, // Paths of directories that are abstracted (have covers)
497
588
  showDirectoryLabels: boolean = true,
498
589
  borderRadius: number = 0, // Border radius for districts (default: sharp corners)
590
+ layerIndex?: LayerIndex, // Optional pre-built index for performance
499
591
  ) {
592
+ // Build index once for all districts (O(n) instead of O(n²))
593
+ const index = layerIndex || new LayerIndex(layers);
594
+
500
595
  districts.forEach(district => {
501
596
  const districtPath = district.path || '';
502
597
  const isRoot = !districtPath || districtPath === '';
503
598
 
504
599
  // Check if this root district has layer matches (like covers) - if so, render it
505
- const rootLayerMatches = getLayerItemsForPath(districtPath, layers, 'exact');
600
+ const rootLayerMatches = index.getItemsForPath(districtPath, 'exact');
506
601
  const hasLayerRendering = rootLayerMatches.length > 0;
507
602
 
508
603
  // Skip root districts unless they have layer rendering (covers, highlights, etc.)
@@ -536,7 +631,7 @@ export function drawLayeredDistricts(
536
631
  let borderOpacity = 0.6;
537
632
 
538
633
  // Check if district has layer highlighting - use exact matching for districts
539
- const layerMatches = getLayerItemsForPath(districtPath, layers, 'exact');
634
+ const layerMatches = index.getItemsForPath(districtPath, 'exact');
540
635
  const hasLayerHighlight = layerMatches.length > 0;
541
636
 
542
637
  if (hasLayerHighlight) {
@@ -805,7 +900,11 @@ export function drawLayeredBuildings(
805
900
  disableOpacityDimming?: boolean,
806
901
  showFileTypeIcons?: boolean,
807
902
  borderRadius: number = 0, // Border radius for buildings (default: 0 - sharp corners)
903
+ layerIndex?: LayerIndex, // Optional pre-built index for performance
808
904
  ) {
905
+ // Build index once for all buildings (O(n) instead of O(n²))
906
+ const index = layerIndex || new LayerIndex(layers);
907
+
809
908
  buildings.forEach(building => {
810
909
  const pos = worldToCanvas(building.position.x, building.position.z);
811
910
 
@@ -832,7 +931,7 @@ export function drawLayeredBuildings(
832
931
  };
833
932
 
834
933
  // Get layer matches for this building - only check file items, not parent directories
835
- const layerMatches = getLayerItemsForPath(building.path, layers).filter(
934
+ const layerMatches = index.getItemsForPath(building.path).filter(
836
935
  match => match.item.type === 'file',
837
936
  ); // Only apply file-specific highlights to buildings
838
937
  const hasLayerHighlight = layerMatches.length > 0;
@@ -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
+ }