@principal-ai/file-city-react 0.4.1 → 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.
- package/dist/components/ArchitectureMapHighlightLayers.d.ts.map +1 -1
- package/dist/components/ArchitectureMapHighlightLayers.js +76 -55
- package/dist/stories/stress-test-data.d.ts +17 -0
- package/dist/stories/stress-test-data.d.ts.map +1 -0
- package/dist/stories/stress-test-data.js +147 -0
- package/package.json +1 -1
- package/src/components/ArchitectureMapHighlightLayers.tsx +83 -66
- package/src/stories/StressTest.stories.tsx +443 -0
- package/src/stories/stress-test-data.ts +176 -0
|
@@ -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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"}
|
|
@@ -210,6 +210,9 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
|
|
|
210
210
|
const isAnimating = targetZoom !== null;
|
|
211
211
|
// Track the last zoomToPath to detect changes
|
|
212
212
|
const lastZoomToPathRef = (0, react_1.useRef)(null);
|
|
213
|
+
// Throttle ref for hover updates (improves performance with large datasets)
|
|
214
|
+
const lastHoverUpdateRef = (0, react_1.useRef)(0);
|
|
215
|
+
const HOVER_THROTTLE_MS = 16; // ~60fps max for hover updates
|
|
213
216
|
(0, react_1.useEffect)(() => {
|
|
214
217
|
// Reset user interaction state when enableZoom is disabled
|
|
215
218
|
if (!enableZoom) {
|
|
@@ -693,6 +696,66 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
|
|
|
693
696
|
}
|
|
694
697
|
return layers;
|
|
695
698
|
}, [stableLayers, dynamicLayers, abstractionLayer]);
|
|
699
|
+
// Memoize abstracted paths lookup - only recalculates when abstraction layer changes
|
|
700
|
+
const { abstractedPathsSet, abstractedPathLookup } = (0, react_1.useMemo)(() => {
|
|
701
|
+
const pathsSet = new Set();
|
|
702
|
+
const abstractionLayerDef = allLayers.find(l => l.id === 'directory-abstraction');
|
|
703
|
+
if (abstractionLayerDef && abstractionLayerDef.enabled) {
|
|
704
|
+
abstractionLayerDef.items.forEach(item => {
|
|
705
|
+
if (item.type === 'directory') {
|
|
706
|
+
pathsSet.add(item.path);
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
abstractedPathsSet: pathsSet,
|
|
712
|
+
abstractedPathLookup: new PathHierarchyLookup(pathsSet),
|
|
713
|
+
};
|
|
714
|
+
}, [allLayers]);
|
|
715
|
+
// Memoize visible districts - only recalculates when data or abstraction changes, NOT on hover
|
|
716
|
+
const visibleDistrictsMemo = (0, react_1.useMemo)(() => {
|
|
717
|
+
if (!filteredCityData)
|
|
718
|
+
return [];
|
|
719
|
+
let districts = abstractedPathLookup.size > 0
|
|
720
|
+
? filteredCityData.districts.filter(district => {
|
|
721
|
+
// Check for root abstraction first
|
|
722
|
+
if (abstractedPathLookup.has('')) {
|
|
723
|
+
return !district.path || district.path === '';
|
|
724
|
+
}
|
|
725
|
+
if (!district.path)
|
|
726
|
+
return true;
|
|
727
|
+
if (abstractedPathLookup.has(district.path))
|
|
728
|
+
return true;
|
|
729
|
+
return !abstractedPathLookup.isChildOfAbstracted(district.path);
|
|
730
|
+
})
|
|
731
|
+
: filteredCityData.districts;
|
|
732
|
+
// If root is abstracted and there's no root district, create one for the cover
|
|
733
|
+
if (abstractedPathsSet.has('')) {
|
|
734
|
+
const hasRootDistrict = districts.some(d => !d.path || d.path === '');
|
|
735
|
+
if (!hasRootDistrict) {
|
|
736
|
+
districts = [
|
|
737
|
+
{
|
|
738
|
+
path: '',
|
|
739
|
+
worldBounds: filteredCityData.bounds,
|
|
740
|
+
fileCount: filteredCityData.buildings.length,
|
|
741
|
+
type: 'directory',
|
|
742
|
+
},
|
|
743
|
+
...districts,
|
|
744
|
+
];
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return districts;
|
|
748
|
+
}, [filteredCityData, abstractedPathLookup, abstractedPathsSet]);
|
|
749
|
+
// Memoize visible buildings - only recalculates when data or abstraction changes, NOT on hover
|
|
750
|
+
const visibleBuildingsMemo = (0, react_1.useMemo)(() => {
|
|
751
|
+
if (!filteredCityData)
|
|
752
|
+
return [];
|
|
753
|
+
return abstractedPathLookup.size > 0
|
|
754
|
+
? filteredCityData.buildings.filter(building => {
|
|
755
|
+
return !abstractedPathLookup.isPathAbstracted(building.path);
|
|
756
|
+
})
|
|
757
|
+
: filteredCityData.buildings;
|
|
758
|
+
}, [filteredCityData, abstractedPathLookup]);
|
|
696
759
|
// Update hit test cache when geometry or abstraction changes
|
|
697
760
|
// Note: We don't depend on zoomState here because:
|
|
698
761
|
// 1. The spatial grid only depends on buildings/districts and abstraction
|
|
@@ -762,64 +825,12 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
|
|
|
762
825
|
y: ((z - coordinateSystemData.bounds.minZ) * scale + offsetZ) * zoomState.scale +
|
|
763
826
|
zoomState.offsetY,
|
|
764
827
|
});
|
|
765
|
-
//
|
|
766
|
-
const abstractedPathsForDistricts = new Set();
|
|
767
|
-
const abstractionLayerForDistricts = allLayers.find(l => l.id === 'directory-abstraction');
|
|
768
|
-
if (abstractionLayerForDistricts && abstractionLayerForDistricts.enabled) {
|
|
769
|
-
abstractionLayerForDistricts.items.forEach(item => {
|
|
770
|
-
if (item.type === 'directory') {
|
|
771
|
-
abstractedPathsForDistricts.add(item.path);
|
|
772
|
-
}
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
// Create PathHierarchyLookup for O(depth) containment checks
|
|
776
|
-
const pathLookup = new PathHierarchyLookup(abstractedPathsForDistricts);
|
|
777
|
-
// Keep abstracted districts (for covers) but filter out their children
|
|
778
|
-
let visibleDistricts = pathLookup.size > 0
|
|
779
|
-
? filteredCityData.districts.filter(district => {
|
|
780
|
-
// Check for root abstraction first
|
|
781
|
-
if (pathLookup.has('')) {
|
|
782
|
-
// If root is abstracted, only show root district
|
|
783
|
-
return !district.path || district.path === '';
|
|
784
|
-
}
|
|
785
|
-
if (!district.path)
|
|
786
|
-
return true; // Keep root
|
|
787
|
-
// Keep the abstracted district itself (we need it for the cover)
|
|
788
|
-
if (pathLookup.has(district.path)) {
|
|
789
|
-
return true;
|
|
790
|
-
}
|
|
791
|
-
// Filter out children of abstracted directories using O(depth) lookup
|
|
792
|
-
return !pathLookup.isChildOfAbstracted(district.path);
|
|
793
|
-
})
|
|
794
|
-
: filteredCityData.districts;
|
|
795
|
-
// If root is abstracted and there's no root district, create one for the cover
|
|
796
|
-
if (abstractedPathsForDistricts.has('')) {
|
|
797
|
-
const hasRootDistrict = visibleDistricts.some(d => !d.path || d.path === '');
|
|
798
|
-
if (!hasRootDistrict) {
|
|
799
|
-
visibleDistricts = [
|
|
800
|
-
{
|
|
801
|
-
path: '',
|
|
802
|
-
worldBounds: filteredCityData.bounds,
|
|
803
|
-
fileCount: filteredCityData.buildings.length, // Total file count
|
|
804
|
-
type: 'directory',
|
|
805
|
-
},
|
|
806
|
-
...visibleDistricts,
|
|
807
|
-
];
|
|
808
|
-
}
|
|
809
|
-
}
|
|
828
|
+
// Use memoized visible districts and buildings (pre-filtered, doesn't recalculate on hover)
|
|
810
829
|
// Draw districts with layer support
|
|
811
|
-
(0, drawLayeredBuildings_1.drawLayeredDistricts)(ctx,
|
|
830
|
+
(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
|
|
812
831
|
showDirectoryLabels, districtBorderRadius);
|
|
813
|
-
// Filter out buildings that are in abstracted directories
|
|
814
|
-
// Reuse the pathLookup created earlier for O(depth) containment checks
|
|
815
|
-
const visibleBuildings = pathLookup.size > 0
|
|
816
|
-
? filteredCityData.buildings.filter(building => {
|
|
817
|
-
// Use PathHierarchyLookup for efficient O(depth) check
|
|
818
|
-
return !pathLookup.isPathAbstracted(building.path);
|
|
819
|
-
})
|
|
820
|
-
: filteredCityData.buildings;
|
|
821
832
|
// Draw buildings with layer support
|
|
822
|
-
(0, drawLayeredBuildings_1.drawLayeredBuildings)(ctx,
|
|
833
|
+
(0, drawLayeredBuildings_1.drawLayeredBuildings)(ctx, visibleBuildingsMemo, worldToCanvas, scale * zoomState.scale, allLayers, interactionState.hoveredBuilding, resolvedDefaultBuildingColor, showFileNames, resolvedHoverBorderColor, disableOpacityDimming, showFileTypeIcons, buildingBorderRadius);
|
|
823
834
|
// Performance monitoring end available for debugging
|
|
824
835
|
// Performance stats available but not logged to reduce console noise
|
|
825
836
|
// Uncomment for debugging: render time, buildings/districts counts, layer counts
|
|
@@ -848,6 +859,10 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
|
|
|
848
859
|
resolvedDefaultBuildingColor,
|
|
849
860
|
districtBorderRadius,
|
|
850
861
|
showFileTypeIcons,
|
|
862
|
+
// Memoized values for performance (don't recalculate on hover)
|
|
863
|
+
visibleDistrictsMemo,
|
|
864
|
+
visibleBuildingsMemo,
|
|
865
|
+
abstractedPathsSet,
|
|
851
866
|
]);
|
|
852
867
|
// Optimized hit testing
|
|
853
868
|
const performHitTest = (0, react_1.useCallback)((canvasX, canvasY) => {
|
|
@@ -974,6 +989,12 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
|
|
|
974
989
|
const handleMouseMoveInternal = (0, react_1.useCallback)((e) => {
|
|
975
990
|
if (!canvasRef.current || !containerRef.current || !filteredCityData || zoomState.isDragging)
|
|
976
991
|
return;
|
|
992
|
+
// Throttle hover updates to improve performance with large datasets
|
|
993
|
+
const now = performance.now();
|
|
994
|
+
if (now - lastHoverUpdateRef.current < HOVER_THROTTLE_MS) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
lastHoverUpdateRef.current = now;
|
|
977
998
|
// Get the container rect for mouse position
|
|
978
999
|
const containerRect = containerRef.current.getBoundingClientRect();
|
|
979
1000
|
// Get mouse position relative to the container
|
|
@@ -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
|
@@ -358,6 +358,10 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
358
358
|
// Track the last zoomToPath to detect changes
|
|
359
359
|
const lastZoomToPathRef = useRef<string | null>(null);
|
|
360
360
|
|
|
361
|
+
// Throttle ref for hover updates (improves performance with large datasets)
|
|
362
|
+
const lastHoverUpdateRef = useRef<number>(0);
|
|
363
|
+
const HOVER_THROTTLE_MS = 16; // ~60fps max for hover updates
|
|
364
|
+
|
|
361
365
|
useEffect(() => {
|
|
362
366
|
// Reset user interaction state when enableZoom is disabled
|
|
363
367
|
if (!enableZoom) {
|
|
@@ -1003,6 +1007,70 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
1003
1007
|
return layers;
|
|
1004
1008
|
}, [stableLayers, dynamicLayers, abstractionLayer]);
|
|
1005
1009
|
|
|
1010
|
+
// Memoize abstracted paths lookup - only recalculates when abstraction layer changes
|
|
1011
|
+
const { abstractedPathsSet, abstractedPathLookup } = useMemo(() => {
|
|
1012
|
+
const pathsSet = new Set<string>();
|
|
1013
|
+
const abstractionLayerDef = allLayers.find(l => l.id === 'directory-abstraction');
|
|
1014
|
+
if (abstractionLayerDef && abstractionLayerDef.enabled) {
|
|
1015
|
+
abstractionLayerDef.items.forEach(item => {
|
|
1016
|
+
if (item.type === 'directory') {
|
|
1017
|
+
pathsSet.add(item.path);
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
return {
|
|
1022
|
+
abstractedPathsSet: pathsSet,
|
|
1023
|
+
abstractedPathLookup: new PathHierarchyLookup(pathsSet),
|
|
1024
|
+
};
|
|
1025
|
+
}, [allLayers]);
|
|
1026
|
+
|
|
1027
|
+
// Memoize visible districts - only recalculates when data or abstraction changes, NOT on hover
|
|
1028
|
+
const visibleDistrictsMemo = useMemo(() => {
|
|
1029
|
+
if (!filteredCityData) return [];
|
|
1030
|
+
|
|
1031
|
+
let districts =
|
|
1032
|
+
abstractedPathLookup.size > 0
|
|
1033
|
+
? filteredCityData.districts.filter(district => {
|
|
1034
|
+
// Check for root abstraction first
|
|
1035
|
+
if (abstractedPathLookup.has('')) {
|
|
1036
|
+
return !district.path || district.path === '';
|
|
1037
|
+
}
|
|
1038
|
+
if (!district.path) return true;
|
|
1039
|
+
if (abstractedPathLookup.has(district.path)) return true;
|
|
1040
|
+
return !abstractedPathLookup.isChildOfAbstracted(district.path);
|
|
1041
|
+
})
|
|
1042
|
+
: filteredCityData.districts;
|
|
1043
|
+
|
|
1044
|
+
// If root is abstracted and there's no root district, create one for the cover
|
|
1045
|
+
if (abstractedPathsSet.has('')) {
|
|
1046
|
+
const hasRootDistrict = districts.some(d => !d.path || d.path === '');
|
|
1047
|
+
if (!hasRootDistrict) {
|
|
1048
|
+
districts = [
|
|
1049
|
+
{
|
|
1050
|
+
path: '',
|
|
1051
|
+
worldBounds: filteredCityData.bounds,
|
|
1052
|
+
fileCount: filteredCityData.buildings.length,
|
|
1053
|
+
type: 'directory' as const,
|
|
1054
|
+
},
|
|
1055
|
+
...districts,
|
|
1056
|
+
];
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return districts;
|
|
1061
|
+
}, [filteredCityData, abstractedPathLookup, abstractedPathsSet]);
|
|
1062
|
+
|
|
1063
|
+
// Memoize visible buildings - only recalculates when data or abstraction changes, NOT on hover
|
|
1064
|
+
const visibleBuildingsMemo = useMemo(() => {
|
|
1065
|
+
if (!filteredCityData) return [];
|
|
1066
|
+
|
|
1067
|
+
return abstractedPathLookup.size > 0
|
|
1068
|
+
? filteredCityData.buildings.filter(building => {
|
|
1069
|
+
return !abstractedPathLookup.isPathAbstracted(building.path);
|
|
1070
|
+
})
|
|
1071
|
+
: filteredCityData.buildings;
|
|
1072
|
+
}, [filteredCityData, abstractedPathLookup]);
|
|
1073
|
+
|
|
1006
1074
|
// Update hit test cache when geometry or abstraction changes
|
|
1007
1075
|
// Note: We don't depend on zoomState here because:
|
|
1008
1076
|
// 1. The spatial grid only depends on buildings/districts and abstraction
|
|
@@ -1106,63 +1174,11 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
1106
1174
|
zoomState.offsetY,
|
|
1107
1175
|
});
|
|
1108
1176
|
|
|
1109
|
-
//
|
|
1110
|
-
const abstractedPathsForDistricts = new Set<string>();
|
|
1111
|
-
const abstractionLayerForDistricts = allLayers.find(l => l.id === 'directory-abstraction');
|
|
1112
|
-
if (abstractionLayerForDistricts && abstractionLayerForDistricts.enabled) {
|
|
1113
|
-
abstractionLayerForDistricts.items.forEach(item => {
|
|
1114
|
-
if (item.type === 'directory') {
|
|
1115
|
-
abstractedPathsForDistricts.add(item.path);
|
|
1116
|
-
}
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
// Create PathHierarchyLookup for O(depth) containment checks
|
|
1121
|
-
const pathLookup = new PathHierarchyLookup(abstractedPathsForDistricts);
|
|
1122
|
-
|
|
1123
|
-
// Keep abstracted districts (for covers) but filter out their children
|
|
1124
|
-
let visibleDistricts =
|
|
1125
|
-
pathLookup.size > 0
|
|
1126
|
-
? filteredCityData.districts.filter(district => {
|
|
1127
|
-
// Check for root abstraction first
|
|
1128
|
-
if (pathLookup.has('')) {
|
|
1129
|
-
// If root is abstracted, only show root district
|
|
1130
|
-
return !district.path || district.path === '';
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
if (!district.path) return true; // Keep root
|
|
1134
|
-
|
|
1135
|
-
// Keep the abstracted district itself (we need it for the cover)
|
|
1136
|
-
if (pathLookup.has(district.path)) {
|
|
1137
|
-
return true;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// Filter out children of abstracted directories using O(depth) lookup
|
|
1141
|
-
return !pathLookup.isChildOfAbstracted(district.path);
|
|
1142
|
-
})
|
|
1143
|
-
: filteredCityData.districts;
|
|
1144
|
-
|
|
1145
|
-
// If root is abstracted and there's no root district, create one for the cover
|
|
1146
|
-
if (abstractedPathsForDistricts.has('')) {
|
|
1147
|
-
const hasRootDistrict = visibleDistricts.some(d => !d.path || d.path === '');
|
|
1148
|
-
|
|
1149
|
-
if (!hasRootDistrict) {
|
|
1150
|
-
visibleDistricts = [
|
|
1151
|
-
{
|
|
1152
|
-
path: '',
|
|
1153
|
-
worldBounds: filteredCityData.bounds,
|
|
1154
|
-
fileCount: filteredCityData.buildings.length, // Total file count
|
|
1155
|
-
type: 'directory',
|
|
1156
|
-
},
|
|
1157
|
-
...visibleDistricts,
|
|
1158
|
-
];
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1177
|
+
// Use memoized visible districts and buildings (pre-filtered, doesn't recalculate on hover)
|
|
1162
1178
|
// Draw districts with layer support
|
|
1163
1179
|
drawLayeredDistricts(
|
|
1164
1180
|
ctx,
|
|
1165
|
-
|
|
1181
|
+
visibleDistrictsMemo,
|
|
1166
1182
|
worldToCanvas,
|
|
1167
1183
|
scale * zoomState.scale,
|
|
1168
1184
|
allLayers,
|
|
@@ -1170,25 +1186,15 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
1170
1186
|
fullSize,
|
|
1171
1187
|
resolvedDefaultDirectoryColor,
|
|
1172
1188
|
filteredCityData.metadata.layoutConfig,
|
|
1173
|
-
|
|
1189
|
+
abstractedPathsSet, // Pass abstracted paths to skip labels
|
|
1174
1190
|
showDirectoryLabels,
|
|
1175
1191
|
districtBorderRadius,
|
|
1176
1192
|
);
|
|
1177
1193
|
|
|
1178
|
-
// Filter out buildings that are in abstracted directories
|
|
1179
|
-
// Reuse the pathLookup created earlier for O(depth) containment checks
|
|
1180
|
-
const visibleBuildings =
|
|
1181
|
-
pathLookup.size > 0
|
|
1182
|
-
? filteredCityData.buildings.filter(building => {
|
|
1183
|
-
// Use PathHierarchyLookup for efficient O(depth) check
|
|
1184
|
-
return !pathLookup.isPathAbstracted(building.path);
|
|
1185
|
-
})
|
|
1186
|
-
: filteredCityData.buildings;
|
|
1187
|
-
|
|
1188
1194
|
// Draw buildings with layer support
|
|
1189
1195
|
drawLayeredBuildings(
|
|
1190
1196
|
ctx,
|
|
1191
|
-
|
|
1197
|
+
visibleBuildingsMemo,
|
|
1192
1198
|
worldToCanvas,
|
|
1193
1199
|
scale * zoomState.scale,
|
|
1194
1200
|
allLayers,
|
|
@@ -1229,6 +1235,10 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
1229
1235
|
resolvedDefaultBuildingColor,
|
|
1230
1236
|
districtBorderRadius,
|
|
1231
1237
|
showFileTypeIcons,
|
|
1238
|
+
// Memoized values for performance (don't recalculate on hover)
|
|
1239
|
+
visibleDistrictsMemo,
|
|
1240
|
+
visibleBuildingsMemo,
|
|
1241
|
+
abstractedPathsSet,
|
|
1232
1242
|
]);
|
|
1233
1243
|
|
|
1234
1244
|
// Optimized hit testing
|
|
@@ -1406,6 +1416,13 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
1406
1416
|
if (!canvasRef.current || !containerRef.current || !filteredCityData || zoomState.isDragging)
|
|
1407
1417
|
return;
|
|
1408
1418
|
|
|
1419
|
+
// Throttle hover updates to improve performance with large datasets
|
|
1420
|
+
const now = performance.now();
|
|
1421
|
+
if (now - lastHoverUpdateRef.current < HOVER_THROTTLE_MS) {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
lastHoverUpdateRef.current = now;
|
|
1425
|
+
|
|
1409
1426
|
// Get the container rect for mouse position
|
|
1410
1427
|
const containerRect = containerRef.current.getBoundingClientRect();
|
|
1411
1428
|
// Get mouse position relative to the container
|
|
@@ -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
|
+
}
|