@principal-ai/file-city-react 0.5.39 → 0.5.40
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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/utils/folderElevatedPanels.d.ts +60 -0
- package/dist/utils/folderElevatedPanels.d.ts.map +1 -0
- package/dist/utils/folderElevatedPanels.js +127 -0
- package/package.json +1 -1
- package/src/index.ts +8 -0
- package/src/stories/ScopeOverlay.stories.tsx +45 -122
- package/src/utils/folderElevatedPanels.ts +170 -0
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export { ThemeProvider, useTheme } from '@principal-ade/industry-theme';
|
|
|
16
16
|
export { FileCity3D, resetCamera, DEFAULT_FLAT_PATTERNS } from './components/FileCity3D';
|
|
17
17
|
export type { FileCity3DProps, AnimationConfig, HighlightLayer as FileCity3DHighlightLayer, IsolationMode, HeightScaling, FlatPattern, ElevatedScopePanel, } from './components/FileCity3D';
|
|
18
18
|
export type { HighlightLayer as FileCity3DHL } from './components/FileCity3D';
|
|
19
|
+
export { buildFolderElevatedPanels, buildFolderIndex, hashFolderColor, } from './utils/folderElevatedPanels';
|
|
20
|
+
export type { BuildFolderElevatedPanelsOptions } from './utils/folderElevatedPanels';
|
|
19
21
|
export { resolveVisualizationIntent } from './utils/visualizationResolution';
|
|
20
22
|
export type { VisualizationIntent, ResolvedVisualizationState, } from './utils/visualizationResolution';
|
|
21
23
|
//# sourceMappingURL=index.d.ts.map
|
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,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,EACrB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAGzF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG7F,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;AAGxE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACzF,YAAY,EACV,eAAe,EACf,eAAe,EACf,cAAc,IAAI,wBAAwB,EAC1C,aAAa,EACb,aAAa,EACb,WAAW,EACX,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AAIjC,YAAY,EAAE,cAAc,IAAI,YAAY,EAAE,MAAM,yBAAyB,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,EACrB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAGzF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG7F,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;AAGxE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACzF,YAAY,EACV,eAAe,EACf,eAAe,EACf,cAAc,IAAI,wBAAwB,EAC1C,aAAa,EACb,aAAa,EACb,WAAW,EACX,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AAIjC,YAAY,EAAE,cAAc,IAAI,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG9E,OAAO,EACL,yBAAyB,EACzB,gBAAgB,EAChB,eAAe,GAChB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,gCAAgC,EAAE,MAAM,8BAA8B,CAAC;AAIrF,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,YAAY,EACV,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,iCAAiC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,8 @@ export { CityViewWithReactFlow, } from './components/CityViewWithReactFlow';
|
|
|
20
20
|
export { ThemeProvider, useTheme } from '@principal-ade/industry-theme';
|
|
21
21
|
// 3D visualization component
|
|
22
22
|
export { FileCity3D, resetCamera, DEFAULT_FLAT_PATTERNS } from './components/FileCity3D';
|
|
23
|
+
// Folder-driven elevated panels (file-tree expansion → 3D umbrella tiles)
|
|
24
|
+
export { buildFolderElevatedPanels, buildFolderIndex, hashFolderColor, } from './utils/folderElevatedPanels';
|
|
23
25
|
// Visualization resolution utilities
|
|
24
26
|
// See docs/VISUALIZATION_STATE_RESOLUTION.md for documentation
|
|
25
27
|
export { resolveVisualizationIntent } from './utils/visualizationResolution';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CityData } from '@principal-ai/file-city-builder';
|
|
2
|
+
import type { ElevatedScopePanel } from '../components/FileCity3D';
|
|
3
|
+
/**
|
|
4
|
+
* Stable color for a folder path, picked from a small palette via a string
|
|
5
|
+
* hash. Two folders at the same depth get visibly different colors; the
|
|
6
|
+
* same folder always gets the same color across renders.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hashFolderColor(path: string): string;
|
|
9
|
+
interface Bounds {
|
|
10
|
+
minX: number;
|
|
11
|
+
maxX: number;
|
|
12
|
+
minZ: number;
|
|
13
|
+
maxZ: number;
|
|
14
|
+
}
|
|
15
|
+
interface FolderIndex {
|
|
16
|
+
/** Parent directory → list of immediate child directories. Top-level folders live under '' (empty string). */
|
|
17
|
+
children: Map<string, string[]>;
|
|
18
|
+
/** Folder path → world bounds spanning every descendant district. */
|
|
19
|
+
bounds: Map<string, Bounds>;
|
|
20
|
+
/** Folder path → number of descendant files. */
|
|
21
|
+
fileCounts: Map<string, number>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Precompute the data structures `buildFolderElevatedPanels` needs from a
|
|
25
|
+
* `CityData`. Cache this when the city data is stable to avoid redoing the
|
|
26
|
+
* O(districts × depth) walk on every render.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildFolderIndex(cityData: CityData): FolderIndex;
|
|
29
|
+
export interface BuildFolderElevatedPanelsOptions {
|
|
30
|
+
cityData: CityData;
|
|
31
|
+
/**
|
|
32
|
+
* Set of folder paths that are currently expanded in the file tree. A folder
|
|
33
|
+
* not in this set is treated as collapsed.
|
|
34
|
+
*/
|
|
35
|
+
expandedFolders: ReadonlySet<string>;
|
|
36
|
+
/** Toggle handler invoked when an umbrella tile is clicked. */
|
|
37
|
+
onToggleFolder?: (folderPath: string) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Scale label font size by descendant file count. Default true. When false,
|
|
40
|
+
* the renderer's auto-sized label is used (size derived from tile footprint).
|
|
41
|
+
*/
|
|
42
|
+
scaleLabelByFileCount?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Pre-built index from `buildFolderIndex(cityData)`. Pass when you cache the
|
|
45
|
+
* city's index across renders to avoid recomputing it.
|
|
46
|
+
*/
|
|
47
|
+
index?: FolderIndex;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Walks the folder hierarchy from the top-level folders of a `CityData`. For
|
|
51
|
+
* each folder:
|
|
52
|
+
* - if expanded → recurse into child folders
|
|
53
|
+
* - if collapsed → emit one elevated panel covering the union of every
|
|
54
|
+
* descendant district's world bounds, colored via `hashFolderColor`.
|
|
55
|
+
*
|
|
56
|
+
* Mirror of the scope-tree expansion behavior, applied to file-tree folders.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildFolderElevatedPanels(options: BuildFolderElevatedPanelsOptions): ElevatedScopePanel[];
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=folderElevatedPanels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folderElevatedPanels.d.ts","sourceRoot":"","sources":["../../src/utils/folderElevatedPanels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAenE;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMpD;AAED,UAAU,MAAM;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,WAAW;IACnB,8GAA8G;IAC9G,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC,qEAAqE;IACrE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,gDAAgD;IAChD,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,CA+ChE;AAED,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,EAAE,QAAQ,CAAC;IACnB;;;OAGG;IACH,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,+DAA+D;IAC/D,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gCAAgC,GACxC,kBAAkB,EAAE,CAsCtB"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const FOLDER_PALETTE = [
|
|
2
|
+
'#3b82f6',
|
|
3
|
+
'#22c55e',
|
|
4
|
+
'#f59e0b',
|
|
5
|
+
'#ec4899',
|
|
6
|
+
'#8b5cf6',
|
|
7
|
+
'#06b6d4',
|
|
8
|
+
'#ef4444',
|
|
9
|
+
'#14b8a6',
|
|
10
|
+
'#a855f7',
|
|
11
|
+
'#eab308',
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Stable color for a folder path, picked from a small palette via a string
|
|
15
|
+
* hash. Two folders at the same depth get visibly different colors; the
|
|
16
|
+
* same folder always gets the same color across renders.
|
|
17
|
+
*/
|
|
18
|
+
export function hashFolderColor(path) {
|
|
19
|
+
let h = 0;
|
|
20
|
+
for (let i = 0; i < path.length; i++) {
|
|
21
|
+
h = (h * 31 + path.charCodeAt(i)) | 0;
|
|
22
|
+
}
|
|
23
|
+
return FOLDER_PALETTE[Math.abs(h) % FOLDER_PALETTE.length];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Precompute the data structures `buildFolderElevatedPanels` needs from a
|
|
27
|
+
* `CityData`. Cache this when the city data is stable to avoid redoing the
|
|
28
|
+
* O(districts × depth) walk on every render.
|
|
29
|
+
*/
|
|
30
|
+
export function buildFolderIndex(cityData) {
|
|
31
|
+
const children = new Map();
|
|
32
|
+
const directorySet = new Set();
|
|
33
|
+
for (const d of cityData.districts)
|
|
34
|
+
directorySet.add(d.path);
|
|
35
|
+
const dirs = Array.from(directorySet).sort();
|
|
36
|
+
for (const dir of dirs) {
|
|
37
|
+
const slash = dir.lastIndexOf('/');
|
|
38
|
+
const parent = slash >= 0 ? dir.slice(0, slash) : '';
|
|
39
|
+
const arr = children.get(parent);
|
|
40
|
+
if (arr)
|
|
41
|
+
arr.push(dir);
|
|
42
|
+
else
|
|
43
|
+
children.set(parent, [dir]);
|
|
44
|
+
}
|
|
45
|
+
const bounds = new Map();
|
|
46
|
+
for (const district of cityData.districts) {
|
|
47
|
+
const b = district.worldBounds;
|
|
48
|
+
let path = district.path;
|
|
49
|
+
while (path) {
|
|
50
|
+
const cur = bounds.get(path);
|
|
51
|
+
if (!cur) {
|
|
52
|
+
bounds.set(path, { minX: b.minX, maxX: b.maxX, minZ: b.minZ, maxZ: b.maxZ });
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
if (b.minX < cur.minX)
|
|
56
|
+
cur.minX = b.minX;
|
|
57
|
+
if (b.maxX > cur.maxX)
|
|
58
|
+
cur.maxX = b.maxX;
|
|
59
|
+
if (b.minZ < cur.minZ)
|
|
60
|
+
cur.minZ = b.minZ;
|
|
61
|
+
if (b.maxZ > cur.maxZ)
|
|
62
|
+
cur.maxZ = b.maxZ;
|
|
63
|
+
}
|
|
64
|
+
const slash = path.lastIndexOf('/');
|
|
65
|
+
if (slash < 0)
|
|
66
|
+
break;
|
|
67
|
+
path = path.slice(0, slash);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const fileCounts = new Map();
|
|
71
|
+
for (const b of cityData.buildings) {
|
|
72
|
+
let path = b.path;
|
|
73
|
+
const slash = path.lastIndexOf('/');
|
|
74
|
+
path = slash >= 0 ? path.slice(0, slash) : '';
|
|
75
|
+
while (path) {
|
|
76
|
+
fileCounts.set(path, (fileCounts.get(path) ?? 0) + 1);
|
|
77
|
+
const s = path.lastIndexOf('/');
|
|
78
|
+
if (s < 0)
|
|
79
|
+
break;
|
|
80
|
+
path = path.slice(0, s);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { children, bounds, fileCounts };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Walks the folder hierarchy from the top-level folders of a `CityData`. For
|
|
87
|
+
* each folder:
|
|
88
|
+
* - if expanded → recurse into child folders
|
|
89
|
+
* - if collapsed → emit one elevated panel covering the union of every
|
|
90
|
+
* descendant district's world bounds, colored via `hashFolderColor`.
|
|
91
|
+
*
|
|
92
|
+
* Mirror of the scope-tree expansion behavior, applied to file-tree folders.
|
|
93
|
+
*/
|
|
94
|
+
export function buildFolderElevatedPanels(options) {
|
|
95
|
+
const { cityData, expandedFolders, onToggleFolder, scaleLabelByFileCount = true, } = options;
|
|
96
|
+
const index = options.index ?? buildFolderIndex(cityData);
|
|
97
|
+
const panels = [];
|
|
98
|
+
const walk = (folderPath) => {
|
|
99
|
+
if (expandedFolders.has(folderPath)) {
|
|
100
|
+
const kids = index.children.get(folderPath) ?? [];
|
|
101
|
+
for (const child of kids)
|
|
102
|
+
walk(child);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const bounds = index.bounds.get(folderPath);
|
|
106
|
+
if (!bounds)
|
|
107
|
+
return;
|
|
108
|
+
const label = folderPath.split('/').pop() ?? folderPath;
|
|
109
|
+
const fileCount = index.fileCounts.get(folderPath) ?? 0;
|
|
110
|
+
const labelSize = scaleLabelByFileCount
|
|
111
|
+
? Math.max(12, Math.min(240, 8 + Math.sqrt(fileCount) * 5))
|
|
112
|
+
: undefined;
|
|
113
|
+
panels.push({
|
|
114
|
+
id: `folder::${folderPath}`,
|
|
115
|
+
color: hashFolderColor(folderPath),
|
|
116
|
+
height: 4,
|
|
117
|
+
thickness: 2,
|
|
118
|
+
bounds,
|
|
119
|
+
label,
|
|
120
|
+
labelSize,
|
|
121
|
+
onClick: onToggleFolder ? () => onToggleFolder(folderPath) : undefined,
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
for (const top of index.children.get('') ?? [])
|
|
125
|
+
walk(top);
|
|
126
|
+
return panels;
|
|
127
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -87,6 +87,14 @@ export type {
|
|
|
87
87
|
// with the 2D HighlightLayer from drawLayeredBuildings
|
|
88
88
|
export type { HighlightLayer as FileCity3DHL } from './components/FileCity3D';
|
|
89
89
|
|
|
90
|
+
// Folder-driven elevated panels (file-tree expansion → 3D umbrella tiles)
|
|
91
|
+
export {
|
|
92
|
+
buildFolderElevatedPanels,
|
|
93
|
+
buildFolderIndex,
|
|
94
|
+
hashFolderColor,
|
|
95
|
+
} from './utils/folderElevatedPanels';
|
|
96
|
+
export type { BuildFolderElevatedPanelsOptions } from './utils/folderElevatedPanels';
|
|
97
|
+
|
|
90
98
|
// Visualization resolution utilities
|
|
91
99
|
// See docs/VISUALIZATION_STATE_RESOLUTION.md for documentation
|
|
92
100
|
export { resolveVisualizationIntent } from './utils/visualizationResolution';
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type HighlightLayer,
|
|
15
15
|
} from '../components/FileCity3D';
|
|
16
16
|
import { createFileColorHighlightLayers } from '../utils/fileColorHighlightLayers';
|
|
17
|
+
import { buildFolderElevatedPanels, buildFolderIndex } from '../utils/folderElevatedPanels';
|
|
17
18
|
|
|
18
19
|
import electronAppCityData from '../../../../assets/electron-app-city-data.json';
|
|
19
20
|
|
|
@@ -117,97 +118,11 @@ const ELECTRON_DISTRICTS_BY_PATH: Map<string, CityDistrict> = new Map(
|
|
|
117
118
|
);
|
|
118
119
|
|
|
119
120
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
121
|
+
* Pre-built folder index (children, bounds, file counts) for the static
|
|
122
|
+
* electron-app city. Cached once at module load so the story doesn't
|
|
123
|
+
* recompute on every render.
|
|
123
124
|
*/
|
|
124
|
-
const
|
|
125
|
-
const m = new Map<string, string[]>();
|
|
126
|
-
const dirs = Array.from(ELECTRON_DIRECTORIES).sort();
|
|
127
|
-
for (const dir of dirs) {
|
|
128
|
-
const slash = dir.lastIndexOf('/');
|
|
129
|
-
const parent = slash >= 0 ? dir.slice(0, slash) : '';
|
|
130
|
-
const arr = m.get(parent);
|
|
131
|
-
if (arr) arr.push(dir);
|
|
132
|
-
else m.set(parent, [dir]);
|
|
133
|
-
}
|
|
134
|
-
return m;
|
|
135
|
-
})();
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Folder path → world bounds spanning every descendant district (including
|
|
139
|
-
* the folder itself, when it is a district). A collapsed folder uses these
|
|
140
|
-
* unioned bounds for its umbrella panel.
|
|
141
|
-
*/
|
|
142
|
-
const ELECTRON_FOLDER_BOUNDS: Map<
|
|
143
|
-
string,
|
|
144
|
-
{ minX: number; maxX: number; minZ: number; maxZ: number }
|
|
145
|
-
> = (() => {
|
|
146
|
-
const m = new Map<
|
|
147
|
-
string,
|
|
148
|
-
{ minX: number; maxX: number; minZ: number; maxZ: number }
|
|
149
|
-
>();
|
|
150
|
-
for (const district of (electronAppCityData as CityData).districts) {
|
|
151
|
-
const b = district.worldBounds;
|
|
152
|
-
let path = district.path;
|
|
153
|
-
while (path) {
|
|
154
|
-
const cur = m.get(path);
|
|
155
|
-
if (!cur) {
|
|
156
|
-
m.set(path, { minX: b.minX, maxX: b.maxX, minZ: b.minZ, maxZ: b.maxZ });
|
|
157
|
-
} else {
|
|
158
|
-
if (b.minX < cur.minX) cur.minX = b.minX;
|
|
159
|
-
if (b.maxX > cur.maxX) cur.maxX = b.maxX;
|
|
160
|
-
if (b.minZ < cur.minZ) cur.minZ = b.minZ;
|
|
161
|
-
if (b.maxZ > cur.maxZ) cur.maxZ = b.maxZ;
|
|
162
|
-
}
|
|
163
|
-
const slash = path.lastIndexOf('/');
|
|
164
|
-
if (slash < 0) break;
|
|
165
|
-
path = path.slice(0, slash);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return m;
|
|
169
|
-
})();
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Folder path → number of descendant files. Used to scale folder-panel
|
|
173
|
-
* label text so larger folders read first when scanning the city.
|
|
174
|
-
*/
|
|
175
|
-
const ELECTRON_FOLDER_FILE_COUNTS: Map<string, number> = (() => {
|
|
176
|
-
const m = new Map<string, number>();
|
|
177
|
-
for (const b of (electronAppCityData as CityData).buildings) {
|
|
178
|
-
let path = b.path;
|
|
179
|
-
const slash = path.lastIndexOf('/');
|
|
180
|
-
path = slash >= 0 ? path.slice(0, slash) : '';
|
|
181
|
-
while (path) {
|
|
182
|
-
m.set(path, (m.get(path) ?? 0) + 1);
|
|
183
|
-
const s = path.lastIndexOf('/');
|
|
184
|
-
if (s < 0) break;
|
|
185
|
-
path = path.slice(0, s);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return m;
|
|
189
|
-
})();
|
|
190
|
-
|
|
191
|
-
const FOLDER_PALETTE = [
|
|
192
|
-
'#3b82f6',
|
|
193
|
-
'#22c55e',
|
|
194
|
-
'#f59e0b',
|
|
195
|
-
'#ec4899',
|
|
196
|
-
'#8b5cf6',
|
|
197
|
-
'#06b6d4',
|
|
198
|
-
'#ef4444',
|
|
199
|
-
'#14b8a6',
|
|
200
|
-
'#a855f7',
|
|
201
|
-
'#eab308',
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
function hashFolderColor(path: string): string {
|
|
205
|
-
let h = 0;
|
|
206
|
-
for (let i = 0; i < path.length; i++) {
|
|
207
|
-
h = (h * 31 + path.charCodeAt(i)) | 0;
|
|
208
|
-
}
|
|
209
|
-
return FOLDER_PALETTE[Math.abs(h) % FOLDER_PALETTE.length];
|
|
210
|
-
}
|
|
125
|
+
const ELECTRON_FOLDER_INDEX = buildFolderIndex(electronAppCityData as CityData);
|
|
211
126
|
|
|
212
127
|
// ---------------------------------------------------------------------------
|
|
213
128
|
// Scope tree paths
|
|
@@ -1194,40 +1109,17 @@ const SingleScopeTemplate: React.FC = () => {
|
|
|
1194
1109
|
);
|
|
1195
1110
|
|
|
1196
1111
|
// Elevated folder panels — driven by the file tree's expansion state.
|
|
1197
|
-
// Recursively walks from the top-level folders: an expanded folder
|
|
1198
|
-
// descends into its child folders; a collapsed folder emits one panel
|
|
1199
|
-
// covering the union of all descendant district bounds.
|
|
1200
1112
|
const folderElevatedPanels = React.useMemo<ElevatedScopePanel[] | undefined>(() => {
|
|
1201
1113
|
if (activeTab !== 'files') return undefined;
|
|
1202
|
-
const panels
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
const label = folderPath.split('/').pop() ?? folderPath;
|
|
1212
|
-
const fileCount = ELECTRON_FOLDER_FILE_COUNTS.get(folderPath) ?? 0;
|
|
1213
|
-
// sqrt growth keeps ~10x file deltas inside a ~3x label-size delta;
|
|
1214
|
-
// the renderer still clamps to the tile footprint.
|
|
1215
|
-
const labelSize = Math.max(12, Math.min(240, 8 + Math.sqrt(fileCount) * 5));
|
|
1216
|
-
panels.push({
|
|
1217
|
-
id: `folder::${folderPath}`,
|
|
1218
|
-
color: hashFolderColor(folderPath),
|
|
1219
|
-
height: 4,
|
|
1220
|
-
thickness: 2,
|
|
1221
|
-
bounds,
|
|
1222
|
-
label,
|
|
1223
|
-
labelSize,
|
|
1224
|
-
onClick: () => {
|
|
1225
|
-
const item = treeModel.getItem(folderPath);
|
|
1226
|
-
if (item?.isDirectory()) item.toggle();
|
|
1227
|
-
},
|
|
1228
|
-
});
|
|
1229
|
-
};
|
|
1230
|
-
for (const top of ELECTRON_FOLDER_CHILDREN.get('') ?? []) walk(top);
|
|
1114
|
+
const panels = buildFolderElevatedPanels({
|
|
1115
|
+
cityData: electronAppCityData as CityData,
|
|
1116
|
+
expandedFolders: folderTreeExpansion.expanded,
|
|
1117
|
+
onToggleFolder: (folderPath) => {
|
|
1118
|
+
const item = treeModel.getItem(folderPath);
|
|
1119
|
+
if (item?.isDirectory()) item.toggle();
|
|
1120
|
+
},
|
|
1121
|
+
index: ELECTRON_FOLDER_INDEX,
|
|
1122
|
+
});
|
|
1231
1123
|
return panels.length > 0 ? panels : undefined;
|
|
1232
1124
|
}, [activeTab, treeModel, folderTreeExpansion]);
|
|
1233
1125
|
|
|
@@ -1647,6 +1539,37 @@ const SingleScopeTemplate: React.FC = () => {
|
|
|
1647
1539
|
showControls={true}
|
|
1648
1540
|
/>
|
|
1649
1541
|
|
|
1542
|
+
{/* Debug overlay — current focusDirectory */}
|
|
1543
|
+
<div
|
|
1544
|
+
style={{
|
|
1545
|
+
position: 'absolute',
|
|
1546
|
+
top: 16,
|
|
1547
|
+
left: 16,
|
|
1548
|
+
padding: '8px 12px',
|
|
1549
|
+
background: 'rgba(15, 23, 42, 0.92)',
|
|
1550
|
+
border: '1px solid #334155',
|
|
1551
|
+
borderRadius: 6,
|
|
1552
|
+
color: '#e2e8f0',
|
|
1553
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1554
|
+
fontSize: 12,
|
|
1555
|
+
zIndex: 100,
|
|
1556
|
+
maxWidth: 480,
|
|
1557
|
+
}}
|
|
1558
|
+
>
|
|
1559
|
+
<div style={sectionLabelStyle}>focusDirectory</div>
|
|
1560
|
+
<div
|
|
1561
|
+
style={{
|
|
1562
|
+
marginTop: 4,
|
|
1563
|
+
fontFamily: 'monospace',
|
|
1564
|
+
fontSize: 12,
|
|
1565
|
+
color: focusDirectory ? '#fde68a' : '#64748b',
|
|
1566
|
+
wordBreak: 'break-all',
|
|
1567
|
+
}}
|
|
1568
|
+
>
|
|
1569
|
+
{focusDirectory ?? '(null)'}
|
|
1570
|
+
</div>
|
|
1571
|
+
</div>
|
|
1572
|
+
|
|
1650
1573
|
{/* Info overlay — driven by scope tree selection */}
|
|
1651
1574
|
{scopeInfo && <ScopeInfoOverlay info={scopeInfo} />}
|
|
1652
1575
|
</div>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { CityData } from '@principal-ai/file-city-builder';
|
|
2
|
+
import type { ElevatedScopePanel } from '../components/FileCity3D';
|
|
3
|
+
|
|
4
|
+
const FOLDER_PALETTE = [
|
|
5
|
+
'#3b82f6',
|
|
6
|
+
'#22c55e',
|
|
7
|
+
'#f59e0b',
|
|
8
|
+
'#ec4899',
|
|
9
|
+
'#8b5cf6',
|
|
10
|
+
'#06b6d4',
|
|
11
|
+
'#ef4444',
|
|
12
|
+
'#14b8a6',
|
|
13
|
+
'#a855f7',
|
|
14
|
+
'#eab308',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Stable color for a folder path, picked from a small palette via a string
|
|
19
|
+
* hash. Two folders at the same depth get visibly different colors; the
|
|
20
|
+
* same folder always gets the same color across renders.
|
|
21
|
+
*/
|
|
22
|
+
export function hashFolderColor(path: string): string {
|
|
23
|
+
let h = 0;
|
|
24
|
+
for (let i = 0; i < path.length; i++) {
|
|
25
|
+
h = (h * 31 + path.charCodeAt(i)) | 0;
|
|
26
|
+
}
|
|
27
|
+
return FOLDER_PALETTE[Math.abs(h) % FOLDER_PALETTE.length];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Bounds {
|
|
31
|
+
minX: number;
|
|
32
|
+
maxX: number;
|
|
33
|
+
minZ: number;
|
|
34
|
+
maxZ: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface FolderIndex {
|
|
38
|
+
/** Parent directory → list of immediate child directories. Top-level folders live under '' (empty string). */
|
|
39
|
+
children: Map<string, string[]>;
|
|
40
|
+
/** Folder path → world bounds spanning every descendant district. */
|
|
41
|
+
bounds: Map<string, Bounds>;
|
|
42
|
+
/** Folder path → number of descendant files. */
|
|
43
|
+
fileCounts: Map<string, number>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Precompute the data structures `buildFolderElevatedPanels` needs from a
|
|
48
|
+
* `CityData`. Cache this when the city data is stable to avoid redoing the
|
|
49
|
+
* O(districts × depth) walk on every render.
|
|
50
|
+
*/
|
|
51
|
+
export function buildFolderIndex(cityData: CityData): FolderIndex {
|
|
52
|
+
const children = new Map<string, string[]>();
|
|
53
|
+
const directorySet = new Set<string>();
|
|
54
|
+
for (const d of cityData.districts) directorySet.add(d.path);
|
|
55
|
+
const dirs = Array.from(directorySet).sort();
|
|
56
|
+
for (const dir of dirs) {
|
|
57
|
+
const slash = dir.lastIndexOf('/');
|
|
58
|
+
const parent = slash >= 0 ? dir.slice(0, slash) : '';
|
|
59
|
+
const arr = children.get(parent);
|
|
60
|
+
if (arr) arr.push(dir);
|
|
61
|
+
else children.set(parent, [dir]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const bounds = new Map<string, Bounds>();
|
|
65
|
+
for (const district of cityData.districts) {
|
|
66
|
+
const b = district.worldBounds;
|
|
67
|
+
let path = district.path;
|
|
68
|
+
while (path) {
|
|
69
|
+
const cur = bounds.get(path);
|
|
70
|
+
if (!cur) {
|
|
71
|
+
bounds.set(path, { minX: b.minX, maxX: b.maxX, minZ: b.minZ, maxZ: b.maxZ });
|
|
72
|
+
} else {
|
|
73
|
+
if (b.minX < cur.minX) cur.minX = b.minX;
|
|
74
|
+
if (b.maxX > cur.maxX) cur.maxX = b.maxX;
|
|
75
|
+
if (b.minZ < cur.minZ) cur.minZ = b.minZ;
|
|
76
|
+
if (b.maxZ > cur.maxZ) cur.maxZ = b.maxZ;
|
|
77
|
+
}
|
|
78
|
+
const slash = path.lastIndexOf('/');
|
|
79
|
+
if (slash < 0) break;
|
|
80
|
+
path = path.slice(0, slash);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const fileCounts = new Map<string, number>();
|
|
85
|
+
for (const b of cityData.buildings) {
|
|
86
|
+
let path = b.path;
|
|
87
|
+
const slash = path.lastIndexOf('/');
|
|
88
|
+
path = slash >= 0 ? path.slice(0, slash) : '';
|
|
89
|
+
while (path) {
|
|
90
|
+
fileCounts.set(path, (fileCounts.get(path) ?? 0) + 1);
|
|
91
|
+
const s = path.lastIndexOf('/');
|
|
92
|
+
if (s < 0) break;
|
|
93
|
+
path = path.slice(0, s);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { children, bounds, fileCounts };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface BuildFolderElevatedPanelsOptions {
|
|
101
|
+
cityData: CityData;
|
|
102
|
+
/**
|
|
103
|
+
* Set of folder paths that are currently expanded in the file tree. A folder
|
|
104
|
+
* not in this set is treated as collapsed.
|
|
105
|
+
*/
|
|
106
|
+
expandedFolders: ReadonlySet<string>;
|
|
107
|
+
/** Toggle handler invoked when an umbrella tile is clicked. */
|
|
108
|
+
onToggleFolder?: (folderPath: string) => void;
|
|
109
|
+
/**
|
|
110
|
+
* Scale label font size by descendant file count. Default true. When false,
|
|
111
|
+
* the renderer's auto-sized label is used (size derived from tile footprint).
|
|
112
|
+
*/
|
|
113
|
+
scaleLabelByFileCount?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Pre-built index from `buildFolderIndex(cityData)`. Pass when you cache the
|
|
116
|
+
* city's index across renders to avoid recomputing it.
|
|
117
|
+
*/
|
|
118
|
+
index?: FolderIndex;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Walks the folder hierarchy from the top-level folders of a `CityData`. For
|
|
123
|
+
* each folder:
|
|
124
|
+
* - if expanded → recurse into child folders
|
|
125
|
+
* - if collapsed → emit one elevated panel covering the union of every
|
|
126
|
+
* descendant district's world bounds, colored via `hashFolderColor`.
|
|
127
|
+
*
|
|
128
|
+
* Mirror of the scope-tree expansion behavior, applied to file-tree folders.
|
|
129
|
+
*/
|
|
130
|
+
export function buildFolderElevatedPanels(
|
|
131
|
+
options: BuildFolderElevatedPanelsOptions,
|
|
132
|
+
): ElevatedScopePanel[] {
|
|
133
|
+
const {
|
|
134
|
+
cityData,
|
|
135
|
+
expandedFolders,
|
|
136
|
+
onToggleFolder,
|
|
137
|
+
scaleLabelByFileCount = true,
|
|
138
|
+
} = options;
|
|
139
|
+
const index = options.index ?? buildFolderIndex(cityData);
|
|
140
|
+
|
|
141
|
+
const panels: ElevatedScopePanel[] = [];
|
|
142
|
+
|
|
143
|
+
const walk = (folderPath: string): void => {
|
|
144
|
+
if (expandedFolders.has(folderPath)) {
|
|
145
|
+
const kids = index.children.get(folderPath) ?? [];
|
|
146
|
+
for (const child of kids) walk(child);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const bounds = index.bounds.get(folderPath);
|
|
150
|
+
if (!bounds) return;
|
|
151
|
+
const label = folderPath.split('/').pop() ?? folderPath;
|
|
152
|
+
const fileCount = index.fileCounts.get(folderPath) ?? 0;
|
|
153
|
+
const labelSize = scaleLabelByFileCount
|
|
154
|
+
? Math.max(12, Math.min(240, 8 + Math.sqrt(fileCount) * 5))
|
|
155
|
+
: undefined;
|
|
156
|
+
panels.push({
|
|
157
|
+
id: `folder::${folderPath}`,
|
|
158
|
+
color: hashFolderColor(folderPath),
|
|
159
|
+
height: 4,
|
|
160
|
+
thickness: 2,
|
|
161
|
+
bounds,
|
|
162
|
+
label,
|
|
163
|
+
labelSize,
|
|
164
|
+
onClick: onToggleFolder ? () => onToggleFolder(folderPath) : undefined,
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
for (const top of index.children.get('') ?? []) walk(top);
|
|
169
|
+
return panels;
|
|
170
|
+
}
|