@principal-ai/file-city-react 0.5.38 → 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/components/FileCity3D/FileCity3D.d.ts +5 -0
- package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
- package/dist/components/FileCity3D/FileCity3D.js +6 -3
- 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/components/FileCity3D/FileCity3D.tsx +12 -2
- package/src/index.ts +8 -0
- package/src/stories/ScopeOverlay.stories.tsx +76 -1
- package/src/utils/folderElevatedPanels.ts +170 -0
|
@@ -63,6 +63,11 @@ export interface ElevatedScopePanel {
|
|
|
63
63
|
label?: string;
|
|
64
64
|
/** Hex color for the label (default white). */
|
|
65
65
|
labelColor?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Absolute label font size in world units. When omitted, falls back to a
|
|
68
|
+
* size derived from the panel's footprint. Always clamped to fit the tile.
|
|
69
|
+
*/
|
|
70
|
+
labelSize?: number;
|
|
66
71
|
/** Click handler. When set, the slab becomes interactive and shows a pointer cursor. */
|
|
67
72
|
onClick?: () => void;
|
|
68
73
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1692,8 +1692,11 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
1692
1692
|
const topY = y + t / 2;
|
|
1693
1693
|
// Size text to the panel: roughly fit longest reasonable label,
|
|
1694
1694
|
// clamped so tiny tiles still render legibly and huge ones don't
|
|
1695
|
-
// get absurd.
|
|
1696
|
-
|
|
1695
|
+
// get absurd. Callers may override via panel.labelSize, but we
|
|
1696
|
+
// still cap to the tile footprint so the label fits.
|
|
1697
|
+
const tileMax = Math.min(w, d) / 2;
|
|
1698
|
+
const requested = panel.labelSize ?? Math.min(w, d) / 6;
|
|
1699
|
+
const labelSize = Math.max(4, Math.min(tileMax, requested));
|
|
1697
1700
|
const handleClick = panel.onClick
|
|
1698
1701
|
? (e) => {
|
|
1699
1702
|
e.stopPropagation();
|
|
@@ -1711,7 +1714,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
1711
1714
|
document.body.style.cursor = '';
|
|
1712
1715
|
}
|
|
1713
1716
|
: undefined;
|
|
1714
|
-
return (_jsxs("group", { children: [_jsxs("mesh", { position: [cx, y, cz], renderOrder: 10, onClick: handleClick, onPointerOver: handlePointerOver, onPointerOut: handlePointerOut, children: [_jsx("boxGeometry", { args: [w, t, d] }), _jsx("meshBasicMaterial", { color: panel.color, transparent: !isOpaque, opacity: opacity, depthWrite: isOpaque })] }), panel.label && (_jsxs(Text, { position: [cx, topY + 0.05, cz], rotation: [-Math.PI / 2, 0, 0], fontSize: labelSize, color: panel.labelColor ?? '#ffffff', anchorX: "center", anchorY: "middle", maxWidth: w * 0.9, textAlign: "center", renderOrder: 11, children: [panel.label, _jsx("meshBasicMaterial", { attach: "material", color: panel.labelColor ?? '#ffffff', depthWrite: false })] }))] }, panel.id));
|
|
1717
|
+
return (_jsxs("group", { children: [_jsxs("mesh", { position: [cx, y, cz], renderOrder: 10, onClick: handleClick, onPointerOver: handlePointerOver, onPointerOut: handlePointerOut, children: [_jsx("boxGeometry", { args: [w, t, d] }), _jsx("meshBasicMaterial", { color: panel.color, transparent: !isOpaque, opacity: opacity, depthWrite: isOpaque })] }), panel.label && (_jsxs(Text, { position: [cx, topY + 0.05, cz], rotation: [-Math.PI / 2, 0, 0], fontSize: labelSize, color: panel.labelColor ?? '#ffffff', anchorX: "center", anchorY: "middle", maxWidth: w * 0.9, textAlign: "center", renderOrder: 11, frustumCulled: false, children: [panel.label, _jsx("meshBasicMaterial", { attach: "material", color: panel.labelColor ?? '#ffffff', depthWrite: false, depthTest: false })] }))] }, panel.id));
|
|
1715
1718
|
})] }));
|
|
1716
1719
|
}
|
|
1717
1720
|
/**
|
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
|
@@ -88,6 +88,11 @@ export interface ElevatedScopePanel {
|
|
|
88
88
|
label?: string;
|
|
89
89
|
/** Hex color for the label (default white). */
|
|
90
90
|
labelColor?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Absolute label font size in world units. When omitted, falls back to a
|
|
93
|
+
* size derived from the panel's footprint. Always clamped to fit the tile.
|
|
94
|
+
*/
|
|
95
|
+
labelSize?: number;
|
|
91
96
|
/** Click handler. When set, the slab becomes interactive and shows a pointer cursor. */
|
|
92
97
|
onClick?: () => void;
|
|
93
98
|
}
|
|
@@ -2666,8 +2671,11 @@ function CityScene({
|
|
|
2666
2671
|
const topY = y + t / 2;
|
|
2667
2672
|
// Size text to the panel: roughly fit longest reasonable label,
|
|
2668
2673
|
// clamped so tiny tiles still render legibly and huge ones don't
|
|
2669
|
-
// get absurd.
|
|
2670
|
-
|
|
2674
|
+
// get absurd. Callers may override via panel.labelSize, but we
|
|
2675
|
+
// still cap to the tile footprint so the label fits.
|
|
2676
|
+
const tileMax = Math.min(w, d) / 2;
|
|
2677
|
+
const requested = panel.labelSize ?? Math.min(w, d) / 6;
|
|
2678
|
+
const labelSize = Math.max(4, Math.min(tileMax, requested));
|
|
2671
2679
|
|
|
2672
2680
|
const handleClick = panel.onClick
|
|
2673
2681
|
? (e: ThreeEvent<MouseEvent>) => {
|
|
@@ -2715,12 +2723,14 @@ function CityScene({
|
|
|
2715
2723
|
maxWidth={w * 0.9}
|
|
2716
2724
|
textAlign="center"
|
|
2717
2725
|
renderOrder={11}
|
|
2726
|
+
frustumCulled={false}
|
|
2718
2727
|
>
|
|
2719
2728
|
{panel.label}
|
|
2720
2729
|
<meshBasicMaterial
|
|
2721
2730
|
attach="material"
|
|
2722
2731
|
color={panel.labelColor ?? '#ffffff'}
|
|
2723
2732
|
depthWrite={false}
|
|
2733
|
+
depthTest={false}
|
|
2724
2734
|
/>
|
|
2725
2735
|
</Text>
|
|
2726
2736
|
)}
|
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
|
|
|
@@ -116,6 +117,13 @@ const ELECTRON_DISTRICTS_BY_PATH: Map<string, CityDistrict> = new Map(
|
|
|
116
117
|
(electronAppCityData as CityData).districts.map(d => [d.path, d]),
|
|
117
118
|
);
|
|
118
119
|
|
|
120
|
+
/**
|
|
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.
|
|
124
|
+
*/
|
|
125
|
+
const ELECTRON_FOLDER_INDEX = buildFolderIndex(electronAppCityData as CityData);
|
|
126
|
+
|
|
119
127
|
// ---------------------------------------------------------------------------
|
|
120
128
|
// Scope tree paths
|
|
121
129
|
// ---------------------------------------------------------------------------
|
|
@@ -1079,6 +1087,42 @@ const SingleScopeTemplate: React.FC = () => {
|
|
|
1079
1087
|
return panels.length > 0 ? panels : undefined;
|
|
1080
1088
|
}, [activeTab, scopes, scopeTreeModel, treeExpansion]);
|
|
1081
1089
|
|
|
1090
|
+
// Track which folders are expanded in the file tree. The file-tree tab's
|
|
1091
|
+
// elevated panels mirror this: a collapsed folder shows one umbrella tile
|
|
1092
|
+
// covering every descendant district; expanding the folder reveals its
|
|
1093
|
+
// sub-folder tiles (or the buildings themselves at the leaves).
|
|
1094
|
+
const folderTreeExpansion = useFileTreeSelector(
|
|
1095
|
+
treeModel,
|
|
1096
|
+
React.useCallback((model: FileTree) => {
|
|
1097
|
+
const expanded = new Set<string>();
|
|
1098
|
+
for (const dir of ELECTRON_DIRECTORIES) {
|
|
1099
|
+
const item = model.getItem(dir);
|
|
1100
|
+
if (item?.isDirectory() && item.isExpanded()) expanded.add(dir);
|
|
1101
|
+
}
|
|
1102
|
+
return { expanded };
|
|
1103
|
+
}, []),
|
|
1104
|
+
React.useCallback((prev, next) => {
|
|
1105
|
+
if (prev.expanded.size !== next.expanded.size) return false;
|
|
1106
|
+
for (const k of prev.expanded) if (!next.expanded.has(k)) return false;
|
|
1107
|
+
return true;
|
|
1108
|
+
}, []),
|
|
1109
|
+
);
|
|
1110
|
+
|
|
1111
|
+
// Elevated folder panels — driven by the file tree's expansion state.
|
|
1112
|
+
const folderElevatedPanels = React.useMemo<ElevatedScopePanel[] | undefined>(() => {
|
|
1113
|
+
if (activeTab !== 'files') return undefined;
|
|
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
|
+
});
|
|
1123
|
+
return panels.length > 0 ? panels : undefined;
|
|
1124
|
+
}, [activeTab, treeModel, folderTreeExpansion]);
|
|
1125
|
+
|
|
1082
1126
|
const openAddModal = React.useCallback((prefillScopeId?: string) => {
|
|
1083
1127
|
setModalScopeId(prefillScopeId ?? '');
|
|
1084
1128
|
setModalNamespaceName('');
|
|
@@ -1484,7 +1528,7 @@ const SingleScopeTemplate: React.FC = () => {
|
|
|
1484
1528
|
linearScale={0.5}
|
|
1485
1529
|
focusDirectory={focusDirectory}
|
|
1486
1530
|
highlightLayers={cityHighlightLayers}
|
|
1487
|
-
elevatedScopePanels={cityElevatedPanels}
|
|
1531
|
+
elevatedScopePanels={cityElevatedPanels ?? folderElevatedPanels}
|
|
1488
1532
|
animation={{
|
|
1489
1533
|
startFlat: true,
|
|
1490
1534
|
autoStartDelay: null,
|
|
@@ -1495,6 +1539,37 @@ const SingleScopeTemplate: React.FC = () => {
|
|
|
1495
1539
|
showControls={true}
|
|
1496
1540
|
/>
|
|
1497
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
|
+
|
|
1498
1573
|
{/* Info overlay — driven by scope tree selection */}
|
|
1499
1574
|
{scopeInfo && <ScopeInfoOverlay info={scopeInfo} />}
|
|
1500
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
|
+
}
|