@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.
- package/dist/components/ArchitectureMapHighlightLayers.d.ts.map +1 -1
- package/dist/components/ArchitectureMapHighlightLayers.js +5 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/render/client/drawLayeredBuildings.d.ts +24 -2
- package/dist/render/client/drawLayeredBuildings.d.ts.map +1 -1
- package/dist/render/client/drawLayeredBuildings.js +93 -5
- 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 +7 -0
- package/src/index.ts +1 -0
- package/src/render/client/drawLayeredBuildings.ts +102 -3
- 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":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAQjF,OAAO,EAIL,cAAc,
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAG7E,MAAM,MAAM,mBAAmB,GAC3B,QAAQ,GACR,MAAM,GACN,MAAM,GACN,SAAS,GACT,OAAO,GACP,MAAM,GACN,QAAQ,CAAC;AAEb,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,cAAc,CAAC,EAAE,mBAAmB,CAAC;IAErC,YAAY,CAAC,EAAE;QACb,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IAEF,YAAY,CAAC,EAAE,CACb,GAAG,EAAE,wBAAwB,EAC7B,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAC/D,KAAK,EAAE,MAAM,KACV,IAAI,CAAC;CACX;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,EAAE,CAAC;IAEnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
@@ -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
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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
|
+
}
|