@principal-ai/file-city-react 0.5.9 → 0.5.10
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 -1
- package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
- package/dist/components/FileCity3D/FileCity3D.js +53 -30
- package/package.json +1 -3
- package/src/components/FileCity3D/FileCity3D.tsx +151 -151
- package/src/stories/FileCity3D.stories.tsx +98 -33
|
@@ -96,6 +96,10 @@ export interface FileCity3DProps {
|
|
|
96
96
|
focusDirectory?: string | null;
|
|
97
97
|
/** Callback when user clicks on a district to navigate */
|
|
98
98
|
onDirectorySelect?: (directory: string | null) => void;
|
|
99
|
+
/** Background color for the canvas container */
|
|
100
|
+
backgroundColor?: string;
|
|
101
|
+
/** Text color for secondary/placeholder text */
|
|
102
|
+
textColor?: string;
|
|
99
103
|
}
|
|
100
104
|
/**
|
|
101
105
|
* FileCity3D - 3D visualization of codebase structure
|
|
@@ -103,6 +107,6 @@ export interface FileCity3DProps {
|
|
|
103
107
|
* Renders CityData as an interactive 3D city where buildings represent files
|
|
104
108
|
* and their height corresponds to line count or file size.
|
|
105
109
|
*/
|
|
106
|
-
export declare function FileCity3D({ cityData, width, height, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls, highlightLayers, isolationMode, dimOpacity, isLoading, loadingMessage, emptyMessage, heightScaling, linearScale, focusDirectory, onDirectorySelect, }: FileCity3DProps): import("react/jsx-runtime").JSX.Element;
|
|
110
|
+
export declare function FileCity3D({ cityData, width, height, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls, highlightLayers, isolationMode, dimOpacity, isLoading, loadingMessage, emptyMessage, heightScaling, linearScale, focusDirectory, onDirectorySelect, backgroundColor, textColor, }: FileCity3DProps): import("react/jsx-runtime").JSX.Element;
|
|
107
111
|
export default FileCity3D;
|
|
108
112
|
//# sourceMappingURL=FileCity3D.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"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"}
|
|
@@ -7,11 +7,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
7
7
|
*
|
|
8
8
|
* Supports animated transition from 2D (flat) to 3D (grown buildings).
|
|
9
9
|
*/
|
|
10
|
-
import { useMemo, useRef, useState, useEffect, useCallback
|
|
10
|
+
import { useMemo, useRef, useState, useEffect, useCallback } from 'react';
|
|
11
11
|
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
|
12
|
-
import { useTheme } from '@principal-ade/industry-theme';
|
|
13
12
|
import { animated, useSpring } from '@react-spring/three';
|
|
14
|
-
import { OrbitControls, PerspectiveCamera, Text, RoundedBox
|
|
13
|
+
import { OrbitControls, PerspectiveCamera, Text, RoundedBox } from '@react-three/drei';
|
|
15
14
|
import { getFileConfig } from '@principal-ai/file-city-builder';
|
|
16
15
|
import * as THREE from 'three';
|
|
17
16
|
const DEFAULT_ANIMATION = {
|
|
@@ -182,8 +181,7 @@ function getHighlightForPath(path, layers) {
|
|
|
182
181
|
if (item.type === 'file' && item.path === path) {
|
|
183
182
|
return { color: layer.color, opacity: layer.opacity ?? 1 };
|
|
184
183
|
}
|
|
185
|
-
if (item.type === 'directory' &&
|
|
186
|
-
(path === item.path || path.startsWith(item.path + '/'))) {
|
|
184
|
+
if (item.type === 'directory' && (path === item.path || path.startsWith(item.path + '/'))) {
|
|
187
185
|
return { color: layer.color, opacity: layer.opacity ?? 1 };
|
|
188
186
|
}
|
|
189
187
|
}
|
|
@@ -191,13 +189,13 @@ function getHighlightForPath(path, layers) {
|
|
|
191
189
|
return null;
|
|
192
190
|
}
|
|
193
191
|
function hasActiveHighlights(layers) {
|
|
194
|
-
return layers.some(
|
|
192
|
+
return layers.some(layer => layer.enabled && layer.items.length > 0);
|
|
195
193
|
}
|
|
196
194
|
// Animated RoundedBox wrapper
|
|
197
195
|
const AnimatedRoundedBox = animated(RoundedBox);
|
|
198
196
|
// Animated meshStandardMaterial for opacity transitions
|
|
199
197
|
const AnimatedMeshStandardMaterial = animated('meshStandardMaterial');
|
|
200
|
-
function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springDuration, heightMultipliersRef }) {
|
|
198
|
+
function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springDuration, heightMultipliersRef, }) {
|
|
201
199
|
const meshRef = useRef(null);
|
|
202
200
|
const startTimeRef = useRef(null);
|
|
203
201
|
const tempObject = useMemo(() => new THREE.Object3D(), []);
|
|
@@ -205,7 +203,7 @@ function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springD
|
|
|
205
203
|
const numEdges = buildings.length * 4;
|
|
206
204
|
// Pre-compute edge data
|
|
207
205
|
const edgeData = useMemo(() => {
|
|
208
|
-
return buildings.flatMap(
|
|
206
|
+
return buildings.flatMap(data => {
|
|
209
207
|
const { width, depth, x, z, fullHeight, staggerDelayMs, buildingIndex } = data;
|
|
210
208
|
const halfW = width / 2;
|
|
211
209
|
const halfD = depth / 2;
|
|
@@ -276,7 +274,8 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
276
274
|
// Initialize height multiplier arrays
|
|
277
275
|
useEffect(() => {
|
|
278
276
|
if (buildings.length > 0) {
|
|
279
|
-
if (!heightMultipliersRef.current ||
|
|
277
|
+
if (!heightMultipliersRef.current ||
|
|
278
|
+
heightMultipliersRef.current.length !== buildings.length) {
|
|
280
279
|
heightMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
281
280
|
targetMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
282
281
|
}
|
|
@@ -323,7 +322,14 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
323
322
|
staggerDelayMs,
|
|
324
323
|
};
|
|
325
324
|
});
|
|
326
|
-
}, [
|
|
325
|
+
}, [
|
|
326
|
+
buildings,
|
|
327
|
+
centerOffset,
|
|
328
|
+
heightScaling,
|
|
329
|
+
linearScale,
|
|
330
|
+
staggerIndices,
|
|
331
|
+
animationConfig.staggerDelay,
|
|
332
|
+
]);
|
|
327
333
|
const minHeight = 0.3;
|
|
328
334
|
const baseOffset = 0.2;
|
|
329
335
|
const tension = animationConfig.tension || 120;
|
|
@@ -510,7 +516,17 @@ function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, l
|
|
|
510
516
|
};
|
|
511
517
|
})
|
|
512
518
|
.filter(Boolean);
|
|
513
|
-
}, [
|
|
519
|
+
}, [
|
|
520
|
+
buildings,
|
|
521
|
+
centerOffset,
|
|
522
|
+
highlightLayers,
|
|
523
|
+
isolationMode,
|
|
524
|
+
hasActiveHighlights,
|
|
525
|
+
heightScaling,
|
|
526
|
+
linearScale,
|
|
527
|
+
staggerIndices,
|
|
528
|
+
staggerDelay,
|
|
529
|
+
]);
|
|
514
530
|
// Don't render if no progress yet
|
|
515
531
|
if (growProgress < 0.1)
|
|
516
532
|
return null;
|
|
@@ -528,7 +544,7 @@ function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, l
|
|
|
528
544
|
return (_jsx(AnimatedIcon, { x: x, z: z, targetHeight: targetHeight, iconSize: iconSize, texture: texture, opacity: opacity, growProgress: growProgress, staggerDelayMs: staggerDelayMs, springDuration: springDuration }, building.path));
|
|
529
545
|
}) }));
|
|
530
546
|
}
|
|
531
|
-
function DistrictFloor({ district, centerOffset, opacity
|
|
547
|
+
function DistrictFloor({ district, centerOffset, opacity }) {
|
|
532
548
|
const { worldBounds } = district;
|
|
533
549
|
const width = worldBounds.maxX - worldBounds.minX;
|
|
534
550
|
const depth = worldBounds.maxZ - worldBounds.minZ;
|
|
@@ -642,9 +658,9 @@ function InfoPanel({ building }) {
|
|
|
642
658
|
marginTop: 4,
|
|
643
659
|
display: 'flex',
|
|
644
660
|
gap: 12,
|
|
645
|
-
}, children: [building.lineCount !== undefined && (_jsxs("span", { children: [building.lineCount.toLocaleString(), " lines"] })), building.size !== undefined &&
|
|
661
|
+
}, children: [building.lineCount !== undefined && (_jsxs("span", { children: [building.lineCount.toLocaleString(), " lines"] })), building.size !== undefined && _jsxs("span", { children: [(building.size / 1024).toFixed(1), " KB"] })] })] }));
|
|
646
662
|
}
|
|
647
|
-
function ControlsOverlay({ isFlat, onToggle, onResetCamera
|
|
663
|
+
function ControlsOverlay({ isFlat, onToggle, onResetCamera }) {
|
|
648
664
|
const buttonStyle = {
|
|
649
665
|
background: 'rgba(15, 23, 42, 0.9)',
|
|
650
666
|
border: '1px solid #334155',
|
|
@@ -747,7 +763,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
747
763
|
const focusTarget = useMemo(() => {
|
|
748
764
|
// Use camera focus directory for camera movement
|
|
749
765
|
if (cameraFocusDirectory) {
|
|
750
|
-
const focusedBuildings = cityData.buildings.filter(
|
|
766
|
+
const focusedBuildings = cityData.buildings.filter(building => isPathInDirectory(building.path, cameraFocusDirectory));
|
|
751
767
|
if (focusedBuildings.length === 0)
|
|
752
768
|
return null;
|
|
753
769
|
let minX = Infinity, maxX = -Infinity;
|
|
@@ -766,10 +782,11 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
766
782
|
const size = Math.max(maxX - minX, maxZ - minZ);
|
|
767
783
|
return { x: centerX, z: centerZ, size };
|
|
768
784
|
}
|
|
769
|
-
// Priority 2: highlight layers
|
|
770
|
-
if
|
|
785
|
+
// Priority 2: highlight layers (only if no focusDirectory is pending)
|
|
786
|
+
// Don't focus on highlights if we're waiting for cameraFocusDirectory to catch up
|
|
787
|
+
if (!activeHighlights || focusDirectory)
|
|
771
788
|
return null;
|
|
772
|
-
const highlightedBuildings = cityData.buildings.filter(
|
|
789
|
+
const highlightedBuildings = cityData.buildings.filter(building => {
|
|
773
790
|
const highlight = getHighlightForPath(building.path, highlightLayers);
|
|
774
791
|
return highlight !== null;
|
|
775
792
|
});
|
|
@@ -790,14 +807,21 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
790
807
|
const centerZ = (minZ + maxZ) / 2;
|
|
791
808
|
const size = Math.max(maxX - minX, maxZ - minZ);
|
|
792
809
|
return { x: centerX, z: centerZ, size };
|
|
793
|
-
}, [
|
|
810
|
+
}, [
|
|
811
|
+
cameraFocusDirectory,
|
|
812
|
+
focusDirectory,
|
|
813
|
+
activeHighlights,
|
|
814
|
+
cityData.buildings,
|
|
815
|
+
highlightLayers,
|
|
816
|
+
centerOffset,
|
|
817
|
+
isPathInDirectory,
|
|
818
|
+
]);
|
|
794
819
|
const staggerIndices = useMemo(() => {
|
|
795
820
|
const centerX = (cityData.bounds.minX + cityData.bounds.maxX) / 2;
|
|
796
821
|
const centerZ = (cityData.bounds.minZ + cityData.bounds.maxZ) / 2;
|
|
797
822
|
const withDistance = cityData.buildings.map((b, originalIndex) => ({
|
|
798
823
|
originalIndex,
|
|
799
|
-
distance: Math.sqrt(Math.pow(b.position.x - centerX, 2) +
|
|
800
|
-
Math.pow(b.position.z - centerZ, 2)),
|
|
824
|
+
distance: Math.sqrt(Math.pow(b.position.x - centerX, 2) + Math.pow(b.position.z - centerZ, 2)),
|
|
801
825
|
}));
|
|
802
826
|
withDistance.sort((a, b) => a.distance - b.distance);
|
|
803
827
|
const indices = new Array(cityData.buildings.length);
|
|
@@ -809,13 +833,13 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
809
833
|
const hoveredIndex = useMemo(() => {
|
|
810
834
|
if (!hoveredBuilding)
|
|
811
835
|
return null;
|
|
812
|
-
return cityData.buildings.findIndex(
|
|
836
|
+
return cityData.buildings.findIndex(b => b.path === hoveredBuilding.path);
|
|
813
837
|
}, [hoveredBuilding, cityData.buildings]);
|
|
814
838
|
// Calculate spring duration for animation sync
|
|
815
839
|
const tension = animationConfig.tension || 120;
|
|
816
840
|
const friction = animationConfig.friction || 14;
|
|
817
841
|
const springDuration = Math.sqrt(1 / (tension * 0.001)) * friction * 20;
|
|
818
|
-
return (_jsxs(_Fragment, { children: [_jsx(AnimatedCamera, { citySize: citySize, isFlat: growProgress === 0, focusTarget: focusTarget }), _jsx("ambientLight", { intensity: 1.2 }), _jsx("hemisphereLight", { args: ['#ddeeff', '#667788', 0.8], position: [0, citySize, 0] }), _jsx("directionalLight", { position: [citySize, citySize * 1.5, citySize * 0.5], intensity: 2, castShadow: true, "shadow-mapSize": [2048, 2048] }), _jsx("directionalLight", { position: [-citySize * 0.5, citySize * 0.8, -citySize * 0.5], intensity: 1 }), _jsx("directionalLight", { position: [citySize * 0.3, citySize, citySize], intensity: 0.6 }), cityData.districts.map(
|
|
842
|
+
return (_jsxs(_Fragment, { children: [_jsx(AnimatedCamera, { citySize: citySize, isFlat: growProgress === 0, focusTarget: focusTarget }), _jsx("ambientLight", { intensity: 1.2 }), _jsx("hemisphereLight", { args: ['#ddeeff', '#667788', 0.8], position: [0, citySize, 0] }), _jsx("directionalLight", { position: [citySize, citySize * 1.5, citySize * 0.5], intensity: 2, castShadow: true, "shadow-mapSize": [2048, 2048] }), _jsx("directionalLight", { position: [-citySize * 0.5, citySize * 0.8, -citySize * 0.5], intensity: 1 }), _jsx("directionalLight", { position: [citySize * 0.3, citySize, citySize], intensity: 0.6 }), cityData.districts.map(district => (_jsx(DistrictFloor, { district: district, centerOffset: centerOffset, opacity: 1 }, district.path))), _jsx(InstancedBuildings, { buildings: cityData.buildings, centerOffset: centerOffset, onHover: onBuildingHover, onClick: onBuildingClick, hoveredIndex: hoveredIndex, growProgress: growProgress, animationConfig: animationConfig, heightScaling: heightScaling, linearScale: linearScale, staggerIndices: staggerIndices, focusDirectory: buildingFocusDirectory, highlightLayers: highlightLayers, isolationMode: isolationMode }), _jsx(BuildingIcons, { buildings: cityData.buildings, centerOffset: centerOffset, growProgress: growProgress, heightScaling: heightScaling, linearScale: linearScale, highlightLayers: highlightLayers, isolationMode: isolationMode, hasActiveHighlights: activeHighlights, staggerIndices: staggerIndices, springDuration: springDuration, staggerDelay: animationConfig.staggerDelay || 15 })] }));
|
|
819
843
|
}
|
|
820
844
|
/**
|
|
821
845
|
* FileCity3D - 3D visualization of codebase structure
|
|
@@ -823,8 +847,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
823
847
|
* Renders CityData as an interactive 3D city where buildings represent files
|
|
824
848
|
* and their height corresponds to line count or file size.
|
|
825
849
|
*/
|
|
826
|
-
export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls = true, highlightLayers = [], isolationMode = 'transparent', dimOpacity = 0.15, isLoading = false, loadingMessage = 'Loading file city...', emptyMessage = 'No file tree data available', heightScaling = 'logarithmic', linearScale = 0.05, focusDirectory = null, onDirectorySelect, }) {
|
|
827
|
-
const { theme } = useTheme();
|
|
850
|
+
export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls = true, highlightLayers = [], isolationMode = 'transparent', dimOpacity = 0.15, isLoading = false, loadingMessage = 'Loading file city...', emptyMessage = 'No file tree data available', heightScaling = 'logarithmic', linearScale = 0.05, focusDirectory = null, onDirectorySelect, backgroundColor = '#0f172a', textColor = '#94a3b8', }) {
|
|
828
851
|
const [hoveredBuilding, setHoveredBuilding] = useState(null);
|
|
829
852
|
const [internalIsGrown, setInternalIsGrown] = useState(false);
|
|
830
853
|
const animationConfig = useMemo(() => ({ ...DEFAULT_ANIMATION, ...animation }), [animation]);
|
|
@@ -854,12 +877,12 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
854
877
|
width,
|
|
855
878
|
height,
|
|
856
879
|
position: 'relative',
|
|
857
|
-
background:
|
|
880
|
+
background: backgroundColor,
|
|
858
881
|
overflow: 'hidden',
|
|
859
882
|
display: 'flex',
|
|
860
883
|
alignItems: 'center',
|
|
861
884
|
justifyContent: 'center',
|
|
862
|
-
color:
|
|
885
|
+
color: textColor,
|
|
863
886
|
fontFamily: 'system-ui, sans-serif',
|
|
864
887
|
fontSize: 14,
|
|
865
888
|
...style,
|
|
@@ -870,12 +893,12 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
870
893
|
width,
|
|
871
894
|
height,
|
|
872
895
|
position: 'relative',
|
|
873
|
-
background:
|
|
896
|
+
background: backgroundColor,
|
|
874
897
|
overflow: 'hidden',
|
|
875
898
|
display: 'flex',
|
|
876
899
|
alignItems: 'center',
|
|
877
900
|
justifyContent: 'center',
|
|
878
|
-
color:
|
|
901
|
+
color: textColor,
|
|
879
902
|
fontFamily: 'system-ui, sans-serif',
|
|
880
903
|
fontSize: 14,
|
|
881
904
|
...style,
|
|
@@ -885,7 +908,7 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
885
908
|
width,
|
|
886
909
|
height,
|
|
887
910
|
position: 'relative',
|
|
888
|
-
background:
|
|
911
|
+
background: backgroundColor,
|
|
889
912
|
overflow: 'hidden',
|
|
890
913
|
...style,
|
|
891
914
|
}, children: [_jsx(Canvas, { shadows: true, style: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principal-ai/file-city-react",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React components for File City visualization",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,13 +22,11 @@
|
|
|
22
22
|
"three": "^0.175.0"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@principal-ade/industry-theme": ">=0.1.3",
|
|
26
25
|
"@principal-ai/alexandria-core-library": ">=0.1.36",
|
|
27
26
|
"@principal-ai/file-city-builder": ">=0.4.5",
|
|
28
27
|
"react": "^19.0.0"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|
|
31
|
-
"@principal-ade/industry-theme": "^0.1.8",
|
|
32
30
|
"@principal-ai/alexandria-core-library": "^0.3.2",
|
|
33
31
|
"@principal-ai/file-city-builder": "^0.4.5",
|
|
34
32
|
"@storybook/addon-docs": "^10.1.2",
|
|
@@ -7,22 +7,11 @@
|
|
|
7
7
|
* Supports animated transition from 2D (flat) to 3D (grown buildings).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import React, {
|
|
11
|
-
useMemo,
|
|
12
|
-
useRef,
|
|
13
|
-
useState,
|
|
14
|
-
useEffect,
|
|
15
|
-
useCallback,
|
|
16
|
-
} from 'react';
|
|
10
|
+
import React, { useMemo, useRef, useState, useEffect, useCallback } from 'react';
|
|
17
11
|
import { Canvas, useFrame, ThreeEvent, useThree } from '@react-three/fiber';
|
|
18
|
-
|
|
12
|
+
|
|
19
13
|
import { animated, useSpring, config } from '@react-spring/three';
|
|
20
|
-
import {
|
|
21
|
-
OrbitControls,
|
|
22
|
-
PerspectiveCamera,
|
|
23
|
-
Text,
|
|
24
|
-
RoundedBox,
|
|
25
|
-
} from '@react-three/drei';
|
|
14
|
+
import { OrbitControls, PerspectiveCamera, Text, RoundedBox } from '@react-three/drei';
|
|
26
15
|
import { getFileConfig } from '@principal-ai/file-city-builder';
|
|
27
16
|
import type {
|
|
28
17
|
CityData,
|
|
@@ -159,7 +148,7 @@ function isCodeFile(extension: string): boolean {
|
|
|
159
148
|
function calculateBuildingHeight(
|
|
160
149
|
building: CityBuilding,
|
|
161
150
|
scaling: HeightScaling = 'logarithmic',
|
|
162
|
-
linearScale: number = 0.05
|
|
151
|
+
linearScale: number = 0.05,
|
|
163
152
|
): number {
|
|
164
153
|
const minHeight = 2;
|
|
165
154
|
|
|
@@ -194,15 +183,23 @@ function calculateBuildingHeight(
|
|
|
194
183
|
const LUCIDE_ICONS: Record<string, string> = {
|
|
195
184
|
Atom: '<circle cx="12" cy="12" r="1"/><path d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"/><path d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"/>',
|
|
196
185
|
Lock: '<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
|
197
|
-
EyeOff:
|
|
186
|
+
EyeOff:
|
|
187
|
+
'<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/>',
|
|
198
188
|
Key: '<path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/>',
|
|
199
|
-
GitBranch:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
189
|
+
GitBranch:
|
|
190
|
+
'<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
|
|
191
|
+
TestTube:
|
|
192
|
+
'<path d="M14.5 2v17.5c0 1.4-1.1 2.5-2.5 2.5c-1.4 0-2.5-1.1-2.5-2.5V2"/><path d="M8.5 2h7"/><path d="M14.5 16h-5"/>',
|
|
193
|
+
FlaskConical:
|
|
194
|
+
'<path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2"/><path d="M8.5 2h7"/><path d="M7 16h10"/>',
|
|
195
|
+
BookText:
|
|
196
|
+
'<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/><path d="M8 11h8"/><path d="M8 7h6"/>',
|
|
197
|
+
BookOpen:
|
|
198
|
+
'<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
|
|
199
|
+
ScrollText:
|
|
200
|
+
'<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
|
|
201
|
+
Settings:
|
|
202
|
+
'<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
|
|
206
203
|
Home: '<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
|
207
204
|
};
|
|
208
205
|
|
|
@@ -283,7 +280,7 @@ function getColorForFile(building: CityBuilding): string {
|
|
|
283
280
|
*/
|
|
284
281
|
function getHighlightForPath(
|
|
285
282
|
path: string,
|
|
286
|
-
layers: HighlightLayer[]
|
|
283
|
+
layers: HighlightLayer[],
|
|
287
284
|
): { color: string; opacity: number } | null {
|
|
288
285
|
for (const layer of layers) {
|
|
289
286
|
if (!layer.enabled) continue;
|
|
@@ -292,10 +289,7 @@ function getHighlightForPath(
|
|
|
292
289
|
if (item.type === 'file' && item.path === path) {
|
|
293
290
|
return { color: layer.color, opacity: layer.opacity ?? 1 };
|
|
294
291
|
}
|
|
295
|
-
if (
|
|
296
|
-
item.type === 'directory' &&
|
|
297
|
-
(path === item.path || path.startsWith(item.path + '/'))
|
|
298
|
-
) {
|
|
292
|
+
if (item.type === 'directory' && (path === item.path || path.startsWith(item.path + '/'))) {
|
|
299
293
|
return { color: layer.color, opacity: layer.opacity ?? 1 };
|
|
300
294
|
}
|
|
301
295
|
}
|
|
@@ -304,7 +298,7 @@ function getHighlightForPath(
|
|
|
304
298
|
}
|
|
305
299
|
|
|
306
300
|
function hasActiveHighlights(layers: HighlightLayer[]): boolean {
|
|
307
|
-
return layers.some(
|
|
301
|
+
return layers.some(layer => layer.enabled && layer.items.length > 0);
|
|
308
302
|
}
|
|
309
303
|
|
|
310
304
|
// Animated RoundedBox wrapper
|
|
@@ -336,7 +330,14 @@ interface BuildingEdgesProps {
|
|
|
336
330
|
heightMultipliersRef: React.MutableRefObject<Float32Array | null>;
|
|
337
331
|
}
|
|
338
332
|
|
|
339
|
-
function BuildingEdges({
|
|
333
|
+
function BuildingEdges({
|
|
334
|
+
buildings,
|
|
335
|
+
growProgress,
|
|
336
|
+
minHeight,
|
|
337
|
+
baseOffset,
|
|
338
|
+
springDuration,
|
|
339
|
+
heightMultipliersRef,
|
|
340
|
+
}: BuildingEdgesProps) {
|
|
340
341
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
|
341
342
|
const startTimeRef = useRef<number | null>(null);
|
|
342
343
|
const tempObject = useMemo(() => new THREE.Object3D(), []);
|
|
@@ -346,7 +347,7 @@ function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springD
|
|
|
346
347
|
|
|
347
348
|
// Pre-compute edge data
|
|
348
349
|
const edgeData = useMemo(() => {
|
|
349
|
-
return buildings.flatMap(
|
|
350
|
+
return buildings.flatMap(data => {
|
|
350
351
|
const { width, depth, x, z, fullHeight, staggerDelayMs, buildingIndex } = data;
|
|
351
352
|
const halfW = width / 2;
|
|
352
353
|
const halfD = depth / 2;
|
|
@@ -471,7 +472,10 @@ function InstancedBuildings({
|
|
|
471
472
|
// Initialize height multiplier arrays
|
|
472
473
|
useEffect(() => {
|
|
473
474
|
if (buildings.length > 0) {
|
|
474
|
-
if (
|
|
475
|
+
if (
|
|
476
|
+
!heightMultipliersRef.current ||
|
|
477
|
+
heightMultipliersRef.current.length !== buildings.length
|
|
478
|
+
) {
|
|
475
479
|
heightMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
476
480
|
targetMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
477
481
|
}
|
|
@@ -525,7 +529,14 @@ function InstancedBuildings({
|
|
|
525
529
|
staggerDelayMs,
|
|
526
530
|
};
|
|
527
531
|
});
|
|
528
|
-
}, [
|
|
532
|
+
}, [
|
|
533
|
+
buildings,
|
|
534
|
+
centerOffset,
|
|
535
|
+
heightScaling,
|
|
536
|
+
linearScale,
|
|
537
|
+
staggerIndices,
|
|
538
|
+
animationConfig.staggerDelay,
|
|
539
|
+
]);
|
|
529
540
|
|
|
530
541
|
const minHeight = 0.3;
|
|
531
542
|
const baseOffset = 0.2;
|
|
@@ -588,7 +599,8 @@ function InstancedBuildings({
|
|
|
588
599
|
// Animate height multiplier towards target
|
|
589
600
|
const currentMultiplier = heightMultipliersRef.current![instanceIndex];
|
|
590
601
|
const targetMultiplier = targetMultipliersRef.current![instanceIndex];
|
|
591
|
-
const newMultiplier =
|
|
602
|
+
const newMultiplier =
|
|
603
|
+
currentMultiplier + (targetMultiplier - currentMultiplier) * collapseSpeed;
|
|
592
604
|
heightMultipliersRef.current![instanceIndex] = newMultiplier;
|
|
593
605
|
|
|
594
606
|
// Calculate grow animation progress
|
|
@@ -646,7 +658,7 @@ function InstancedBuildings({
|
|
|
646
658
|
onHover?.(data.building);
|
|
647
659
|
}
|
|
648
660
|
},
|
|
649
|
-
[buildingData, onHover]
|
|
661
|
+
[buildingData, onHover],
|
|
650
662
|
);
|
|
651
663
|
|
|
652
664
|
const handlePointerOut = useCallback(() => {
|
|
@@ -661,7 +673,7 @@ function InstancedBuildings({
|
|
|
661
673
|
onClick?.(data.building);
|
|
662
674
|
}
|
|
663
675
|
},
|
|
664
|
-
[buildingData, onClick]
|
|
676
|
+
[buildingData, onClick],
|
|
665
677
|
);
|
|
666
678
|
|
|
667
679
|
if (buildingData.length === 0) return null;
|
|
@@ -784,11 +796,7 @@ function AnimatedIcon({
|
|
|
784
796
|
});
|
|
785
797
|
|
|
786
798
|
return (
|
|
787
|
-
<sprite
|
|
788
|
-
ref={spriteRef}
|
|
789
|
-
position={[x, 0, z]}
|
|
790
|
-
scale={[iconSize, iconSize, 1]}
|
|
791
|
-
>
|
|
799
|
+
<sprite ref={spriteRef} position={[x, 0, z]} scale={[iconSize, iconSize, 1]}>
|
|
792
800
|
<spriteMaterial
|
|
793
801
|
ref={materialRef}
|
|
794
802
|
map={texture}
|
|
@@ -849,49 +857,61 @@ function BuildingIcons({
|
|
|
849
857
|
};
|
|
850
858
|
})
|
|
851
859
|
.filter(Boolean) as Array<{
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}, [
|
|
860
|
+
building: CityBuilding;
|
|
861
|
+
config: FileConfigResult;
|
|
862
|
+
x: number;
|
|
863
|
+
z: number;
|
|
864
|
+
targetHeight: number;
|
|
865
|
+
shouldDim: boolean;
|
|
866
|
+
staggerDelayMs: number;
|
|
867
|
+
}>;
|
|
868
|
+
}, [
|
|
869
|
+
buildings,
|
|
870
|
+
centerOffset,
|
|
871
|
+
highlightLayers,
|
|
872
|
+
isolationMode,
|
|
873
|
+
hasActiveHighlights,
|
|
874
|
+
heightScaling,
|
|
875
|
+
linearScale,
|
|
876
|
+
staggerIndices,
|
|
877
|
+
staggerDelay,
|
|
878
|
+
]);
|
|
861
879
|
|
|
862
880
|
// Don't render if no progress yet
|
|
863
881
|
if (growProgress < 0.1) return null;
|
|
864
882
|
|
|
865
883
|
return (
|
|
866
884
|
<>
|
|
867
|
-
{buildingsWithIcons.map(
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
885
|
+
{buildingsWithIcons.map(
|
|
886
|
+
({ building, config, x, z, targetHeight, shouldDim, staggerDelayMs }) => {
|
|
887
|
+
const icon = config.icon!;
|
|
888
|
+
const texture = getIconTexture(icon.name, icon.color || '#ffffff');
|
|
889
|
+
if (!texture) return null;
|
|
890
|
+
|
|
891
|
+
// Icon size based on building dimensions
|
|
892
|
+
const [width] = building.dimensions;
|
|
893
|
+
const baseSize = Math.max(width * 0.8, 6);
|
|
894
|
+
const heightBoost = Math.min(targetHeight / 20, 3);
|
|
895
|
+
const iconSize = (baseSize + heightBoost) * (icon.size || 1);
|
|
896
|
+
|
|
897
|
+
const opacity = shouldDim && isolationMode === 'transparent' ? 0.3 : 1;
|
|
898
|
+
|
|
899
|
+
return (
|
|
900
|
+
<AnimatedIcon
|
|
901
|
+
key={building.path}
|
|
902
|
+
x={x}
|
|
903
|
+
z={z}
|
|
904
|
+
targetHeight={targetHeight}
|
|
905
|
+
iconSize={iconSize}
|
|
906
|
+
texture={texture}
|
|
907
|
+
opacity={opacity}
|
|
908
|
+
growProgress={growProgress}
|
|
909
|
+
staggerDelayMs={staggerDelayMs}
|
|
910
|
+
springDuration={springDuration}
|
|
911
|
+
/>
|
|
912
|
+
);
|
|
913
|
+
},
|
|
914
|
+
)}
|
|
895
915
|
</>
|
|
896
916
|
);
|
|
897
917
|
}
|
|
@@ -903,11 +923,7 @@ interface DistrictFloorProps {
|
|
|
903
923
|
opacity: number;
|
|
904
924
|
}
|
|
905
925
|
|
|
906
|
-
function DistrictFloor({
|
|
907
|
-
district,
|
|
908
|
-
centerOffset,
|
|
909
|
-
opacity,
|
|
910
|
-
}: DistrictFloorProps) {
|
|
926
|
+
function DistrictFloor({ district, centerOffset, opacity }: DistrictFloorProps) {
|
|
911
927
|
const { worldBounds } = district;
|
|
912
928
|
const width = worldBounds.maxX - worldBounds.minX;
|
|
913
929
|
const depth = worldBounds.maxZ - worldBounds.minZ;
|
|
@@ -921,15 +937,8 @@ function DistrictFloor({
|
|
|
921
937
|
|
|
922
938
|
return (
|
|
923
939
|
<group position={[centerX, 0, centerZ]}>
|
|
924
|
-
<lineSegments
|
|
925
|
-
|
|
926
|
-
position={[0, floorY, 0]}
|
|
927
|
-
renderOrder={-1}
|
|
928
|
-
>
|
|
929
|
-
<edgesGeometry
|
|
930
|
-
args={[new THREE.PlaneGeometry(width, depth)]}
|
|
931
|
-
attach="geometry"
|
|
932
|
-
/>
|
|
940
|
+
<lineSegments rotation={[-Math.PI / 2, 0, 0]} position={[0, floorY, 0]} renderOrder={-1}>
|
|
941
|
+
<edgesGeometry args={[new THREE.PlaneGeometry(width, depth)]} attach="geometry" />
|
|
933
942
|
<lineBasicMaterial color="#475569" depthWrite={false} />
|
|
934
943
|
</lineSegments>
|
|
935
944
|
|
|
@@ -955,7 +964,7 @@ function DistrictFloor({
|
|
|
955
964
|
interface FocusTarget {
|
|
956
965
|
x: number;
|
|
957
966
|
z: number;
|
|
958
|
-
size: number;
|
|
967
|
+
size: number; // Approximate size of the focused area
|
|
959
968
|
}
|
|
960
969
|
|
|
961
970
|
interface AnimatedCameraProps {
|
|
@@ -1074,8 +1083,7 @@ function InfoPanel({ building }: InfoPanelProps) {
|
|
|
1074
1083
|
|
|
1075
1084
|
const fileName = building.path.split('/').pop();
|
|
1076
1085
|
const dirPath = building.path.split('/').slice(0, -1).join('/');
|
|
1077
|
-
const rawExt =
|
|
1078
|
-
building.fileExtension || building.path.split('.').pop() || '';
|
|
1086
|
+
const rawExt = building.fileExtension || building.path.split('.').pop() || '';
|
|
1079
1087
|
const ext = rawExt.replace(/^\./, '');
|
|
1080
1088
|
const isCode = isCodeFile(ext);
|
|
1081
1089
|
|
|
@@ -1110,9 +1118,7 @@ function InfoPanel({ building }: InfoPanelProps) {
|
|
|
1110
1118
|
{building.lineCount !== undefined && (
|
|
1111
1119
|
<span>{building.lineCount.toLocaleString()} lines</span>
|
|
1112
1120
|
)}
|
|
1113
|
-
{building.size !== undefined && (
|
|
1114
|
-
<span>{(building.size / 1024).toFixed(1)} KB</span>
|
|
1115
|
-
)}
|
|
1121
|
+
{building.size !== undefined && <span>{(building.size / 1024).toFixed(1)} KB</span>}
|
|
1116
1122
|
</div>
|
|
1117
1123
|
</div>
|
|
1118
1124
|
);
|
|
@@ -1125,11 +1131,7 @@ interface ControlsOverlayProps {
|
|
|
1125
1131
|
onResetCamera: () => void;
|
|
1126
1132
|
}
|
|
1127
1133
|
|
|
1128
|
-
function ControlsOverlay({
|
|
1129
|
-
isFlat,
|
|
1130
|
-
onToggle,
|
|
1131
|
-
onResetCamera,
|
|
1132
|
-
}: ControlsOverlayProps) {
|
|
1134
|
+
function ControlsOverlay({ isFlat, onToggle, onResetCamera }: ControlsOverlayProps) {
|
|
1133
1135
|
const buttonStyle = {
|
|
1134
1136
|
background: 'rgba(15, 23, 42, 0.9)',
|
|
1135
1137
|
border: '1px solid #334155',
|
|
@@ -1196,18 +1198,15 @@ function CityScene({
|
|
|
1196
1198
|
x: (cityData.bounds.minX + cityData.bounds.maxX) / 2,
|
|
1197
1199
|
z: (cityData.bounds.minZ + cityData.bounds.maxZ) / 2,
|
|
1198
1200
|
}),
|
|
1199
|
-
[cityData.bounds]
|
|
1201
|
+
[cityData.bounds],
|
|
1200
1202
|
);
|
|
1201
1203
|
|
|
1202
1204
|
const citySize = Math.max(
|
|
1203
1205
|
cityData.bounds.maxX - cityData.bounds.minX,
|
|
1204
|
-
cityData.bounds.maxZ - cityData.bounds.minZ
|
|
1206
|
+
cityData.bounds.maxZ - cityData.bounds.minZ,
|
|
1205
1207
|
);
|
|
1206
1208
|
|
|
1207
|
-
const activeHighlights = useMemo(
|
|
1208
|
-
() => hasActiveHighlights(highlightLayers),
|
|
1209
|
-
[highlightLayers]
|
|
1210
|
-
);
|
|
1209
|
+
const activeHighlights = useMemo(() => hasActiveHighlights(highlightLayers), [highlightLayers]);
|
|
1211
1210
|
|
|
1212
1211
|
// Helper to check if a path is inside a directory
|
|
1213
1212
|
const isPathInDirectory = useCallback((path: string, directory: string) => {
|
|
@@ -1291,14 +1290,16 @@ function CityScene({
|
|
|
1291
1290
|
const focusTarget = useMemo((): FocusTarget | null => {
|
|
1292
1291
|
// Use camera focus directory for camera movement
|
|
1293
1292
|
if (cameraFocusDirectory) {
|
|
1294
|
-
const focusedBuildings = cityData.buildings.filter(
|
|
1295
|
-
isPathInDirectory(building.path, cameraFocusDirectory)
|
|
1293
|
+
const focusedBuildings = cityData.buildings.filter(building =>
|
|
1294
|
+
isPathInDirectory(building.path, cameraFocusDirectory),
|
|
1296
1295
|
);
|
|
1297
1296
|
|
|
1298
1297
|
if (focusedBuildings.length === 0) return null;
|
|
1299
1298
|
|
|
1300
|
-
let minX = Infinity,
|
|
1301
|
-
|
|
1299
|
+
let minX = Infinity,
|
|
1300
|
+
maxX = -Infinity;
|
|
1301
|
+
let minZ = Infinity,
|
|
1302
|
+
maxZ = -Infinity;
|
|
1302
1303
|
|
|
1303
1304
|
for (const building of focusedBuildings) {
|
|
1304
1305
|
const x = building.position.x - centerOffset.x;
|
|
@@ -1318,18 +1319,21 @@ function CityScene({
|
|
|
1318
1319
|
return { x: centerX, z: centerZ, size };
|
|
1319
1320
|
}
|
|
1320
1321
|
|
|
1321
|
-
// Priority 2: highlight layers
|
|
1322
|
-
if
|
|
1322
|
+
// Priority 2: highlight layers (only if no focusDirectory is pending)
|
|
1323
|
+
// Don't focus on highlights if we're waiting for cameraFocusDirectory to catch up
|
|
1324
|
+
if (!activeHighlights || focusDirectory) return null;
|
|
1323
1325
|
|
|
1324
|
-
const highlightedBuildings = cityData.buildings.filter(
|
|
1326
|
+
const highlightedBuildings = cityData.buildings.filter(building => {
|
|
1325
1327
|
const highlight = getHighlightForPath(building.path, highlightLayers);
|
|
1326
1328
|
return highlight !== null;
|
|
1327
1329
|
});
|
|
1328
1330
|
|
|
1329
1331
|
if (highlightedBuildings.length === 0) return null;
|
|
1330
1332
|
|
|
1331
|
-
let minX = Infinity,
|
|
1332
|
-
|
|
1333
|
+
let minX = Infinity,
|
|
1334
|
+
maxX = -Infinity;
|
|
1335
|
+
let minZ = Infinity,
|
|
1336
|
+
maxZ = -Infinity;
|
|
1333
1337
|
|
|
1334
1338
|
for (const building of highlightedBuildings) {
|
|
1335
1339
|
const x = building.position.x - centerOffset.x;
|
|
@@ -1347,7 +1351,15 @@ function CityScene({
|
|
|
1347
1351
|
const size = Math.max(maxX - minX, maxZ - minZ);
|
|
1348
1352
|
|
|
1349
1353
|
return { x: centerX, z: centerZ, size };
|
|
1350
|
-
}, [
|
|
1354
|
+
}, [
|
|
1355
|
+
cameraFocusDirectory,
|
|
1356
|
+
focusDirectory,
|
|
1357
|
+
activeHighlights,
|
|
1358
|
+
cityData.buildings,
|
|
1359
|
+
highlightLayers,
|
|
1360
|
+
centerOffset,
|
|
1361
|
+
isPathInDirectory,
|
|
1362
|
+
]);
|
|
1351
1363
|
|
|
1352
1364
|
const staggerIndices = useMemo(() => {
|
|
1353
1365
|
const centerX = (cityData.bounds.minX + cityData.bounds.maxX) / 2;
|
|
@@ -1356,8 +1368,7 @@ function CityScene({
|
|
|
1356
1368
|
const withDistance = cityData.buildings.map((b, originalIndex) => ({
|
|
1357
1369
|
originalIndex,
|
|
1358
1370
|
distance: Math.sqrt(
|
|
1359
|
-
Math.pow(b.position.x - centerX, 2) +
|
|
1360
|
-
Math.pow(b.position.z - centerZ, 2)
|
|
1371
|
+
Math.pow(b.position.x - centerX, 2) + Math.pow(b.position.z - centerZ, 2),
|
|
1361
1372
|
),
|
|
1362
1373
|
}));
|
|
1363
1374
|
|
|
@@ -1373,7 +1384,7 @@ function CityScene({
|
|
|
1373
1384
|
|
|
1374
1385
|
const hoveredIndex = useMemo(() => {
|
|
1375
1386
|
if (!hoveredBuilding) return null;
|
|
1376
|
-
return cityData.buildings.findIndex(
|
|
1387
|
+
return cityData.buildings.findIndex(b => b.path === hoveredBuilding.path);
|
|
1377
1388
|
}, [hoveredBuilding, cityData.buildings]);
|
|
1378
1389
|
|
|
1379
1390
|
// Calculate spring duration for animation sync
|
|
@@ -1386,10 +1397,7 @@ function CityScene({
|
|
|
1386
1397
|
<AnimatedCamera citySize={citySize} isFlat={growProgress === 0} focusTarget={focusTarget} />
|
|
1387
1398
|
|
|
1388
1399
|
<ambientLight intensity={1.2} />
|
|
1389
|
-
<hemisphereLight
|
|
1390
|
-
args={['#ddeeff', '#667788', 0.8]}
|
|
1391
|
-
position={[0, citySize, 0]}
|
|
1392
|
-
/>
|
|
1400
|
+
<hemisphereLight args={['#ddeeff', '#667788', 0.8]} position={[0, citySize, 0]} />
|
|
1393
1401
|
<directionalLight
|
|
1394
1402
|
position={[citySize, citySize * 1.5, citySize * 0.5]}
|
|
1395
1403
|
intensity={2}
|
|
@@ -1400,12 +1408,9 @@ function CityScene({
|
|
|
1400
1408
|
position={[-citySize * 0.5, citySize * 0.8, -citySize * 0.5]}
|
|
1401
1409
|
intensity={1}
|
|
1402
1410
|
/>
|
|
1403
|
-
<directionalLight
|
|
1404
|
-
position={[citySize * 0.3, citySize, citySize]}
|
|
1405
|
-
intensity={0.6}
|
|
1406
|
-
/>
|
|
1411
|
+
<directionalLight position={[citySize * 0.3, citySize, citySize]} intensity={0.6} />
|
|
1407
1412
|
|
|
1408
|
-
{cityData.districts.map(
|
|
1413
|
+
{cityData.districts.map(district => (
|
|
1409
1414
|
<DistrictFloor
|
|
1410
1415
|
key={district.path}
|
|
1411
1416
|
district={district}
|
|
@@ -1492,6 +1497,10 @@ export interface FileCity3DProps {
|
|
|
1492
1497
|
focusDirectory?: string | null;
|
|
1493
1498
|
/** Callback when user clicks on a district to navigate */
|
|
1494
1499
|
onDirectorySelect?: (directory: string | null) => void;
|
|
1500
|
+
/** Background color for the canvas container */
|
|
1501
|
+
backgroundColor?: string;
|
|
1502
|
+
/** Text color for secondary/placeholder text */
|
|
1503
|
+
textColor?: string;
|
|
1495
1504
|
}
|
|
1496
1505
|
|
|
1497
1506
|
/**
|
|
@@ -1521,20 +1530,15 @@ export function FileCity3D({
|
|
|
1521
1530
|
linearScale = 0.05,
|
|
1522
1531
|
focusDirectory = null,
|
|
1523
1532
|
onDirectorySelect,
|
|
1533
|
+
backgroundColor = '#0f172a',
|
|
1534
|
+
textColor = '#94a3b8',
|
|
1524
1535
|
}: FileCity3DProps) {
|
|
1525
|
-
const
|
|
1526
|
-
const [hoveredBuilding, setHoveredBuilding] = useState<CityBuilding | null>(
|
|
1527
|
-
null
|
|
1528
|
-
);
|
|
1536
|
+
const [hoveredBuilding, setHoveredBuilding] = useState<CityBuilding | null>(null);
|
|
1529
1537
|
const [internalIsGrown, setInternalIsGrown] = useState(false);
|
|
1530
1538
|
|
|
1531
|
-
const animationConfig = useMemo(
|
|
1532
|
-
() => ({ ...DEFAULT_ANIMATION, ...animation }),
|
|
1533
|
-
[animation]
|
|
1534
|
-
);
|
|
1539
|
+
const animationConfig = useMemo(() => ({ ...DEFAULT_ANIMATION, ...animation }), [animation]);
|
|
1535
1540
|
|
|
1536
|
-
const isGrown =
|
|
1537
|
-
externalIsGrown !== undefined ? externalIsGrown : internalIsGrown;
|
|
1541
|
+
const isGrown = externalIsGrown !== undefined ? externalIsGrown : internalIsGrown;
|
|
1538
1542
|
const setIsGrown = (value: boolean) => {
|
|
1539
1543
|
setInternalIsGrown(value);
|
|
1540
1544
|
onGrowChange?.(value);
|
|
@@ -1566,12 +1570,12 @@ export function FileCity3D({
|
|
|
1566
1570
|
width,
|
|
1567
1571
|
height,
|
|
1568
1572
|
position: 'relative',
|
|
1569
|
-
background:
|
|
1573
|
+
background: backgroundColor,
|
|
1570
1574
|
overflow: 'hidden',
|
|
1571
1575
|
display: 'flex',
|
|
1572
1576
|
alignItems: 'center',
|
|
1573
1577
|
justifyContent: 'center',
|
|
1574
|
-
color:
|
|
1578
|
+
color: textColor,
|
|
1575
1579
|
fontFamily: 'system-ui, sans-serif',
|
|
1576
1580
|
fontSize: 14,
|
|
1577
1581
|
...style,
|
|
@@ -1590,12 +1594,12 @@ export function FileCity3D({
|
|
|
1590
1594
|
width,
|
|
1591
1595
|
height,
|
|
1592
1596
|
position: 'relative',
|
|
1593
|
-
background:
|
|
1597
|
+
background: backgroundColor,
|
|
1594
1598
|
overflow: 'hidden',
|
|
1595
1599
|
display: 'flex',
|
|
1596
1600
|
alignItems: 'center',
|
|
1597
1601
|
justifyContent: 'center',
|
|
1598
|
-
color:
|
|
1602
|
+
color: textColor,
|
|
1599
1603
|
fontFamily: 'system-ui, sans-serif',
|
|
1600
1604
|
fontSize: 14,
|
|
1601
1605
|
...style,
|
|
@@ -1613,7 +1617,7 @@ export function FileCity3D({
|
|
|
1613
1617
|
width,
|
|
1614
1618
|
height,
|
|
1615
1619
|
position: 'relative',
|
|
1616
|
-
background:
|
|
1620
|
+
background: backgroundColor,
|
|
1617
1621
|
overflow: 'hidden',
|
|
1618
1622
|
...style,
|
|
1619
1623
|
}}
|
|
@@ -1644,11 +1648,7 @@ export function FileCity3D({
|
|
|
1644
1648
|
</Canvas>
|
|
1645
1649
|
<InfoPanel building={hoveredBuilding} />
|
|
1646
1650
|
{showControls && (
|
|
1647
|
-
<ControlsOverlay
|
|
1648
|
-
isFlat={!isGrown}
|
|
1649
|
-
onToggle={handleToggle}
|
|
1650
|
-
onResetCamera={resetCamera}
|
|
1651
|
-
/>
|
|
1651
|
+
<ControlsOverlay isFlat={!isGrown} onToggle={handleToggle} onResetCamera={resetCamera} />
|
|
1652
1652
|
)}
|
|
1653
1653
|
</div>
|
|
1654
1654
|
);
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
FileCity3D,
|
|
5
|
+
type CityData,
|
|
6
|
+
type CityBuilding,
|
|
7
|
+
type CityDistrict,
|
|
8
|
+
type HighlightLayer,
|
|
9
|
+
type IsolationMode,
|
|
10
|
+
} from '../components/FileCity3D';
|
|
5
11
|
|
|
6
12
|
const meta: Meta<typeof FileCity3D> = {
|
|
7
13
|
title: 'Components/FileCity3D',
|
|
8
14
|
component: FileCity3D,
|
|
9
|
-
decorators: [
|
|
10
|
-
(Story) => (
|
|
11
|
-
<ThemeProvider>
|
|
12
|
-
<Story />
|
|
13
|
-
</ThemeProvider>
|
|
14
|
-
),
|
|
15
|
-
],
|
|
16
15
|
parameters: {
|
|
17
16
|
layout: 'fullscreen',
|
|
18
17
|
},
|
|
@@ -40,7 +39,7 @@ function generateBuildings(
|
|
|
40
39
|
startX: number,
|
|
41
40
|
startZ: number,
|
|
42
41
|
areaWidth: number,
|
|
43
|
-
areaDepth: number
|
|
42
|
+
areaDepth: number,
|
|
44
43
|
): CityBuilding[] {
|
|
45
44
|
const buildings: CityBuilding[] = [];
|
|
46
45
|
const allExtensions = [...CODE_EXTENSIONS, ...NON_CODE_EXTENSIONS];
|
|
@@ -56,9 +55,7 @@ function generateBuildings(
|
|
|
56
55
|
const lineCount = isCode
|
|
57
56
|
? Math.floor(Math.exp(Math.random() * Math.log(3000 - 20) + Math.log(20)))
|
|
58
57
|
: undefined;
|
|
59
|
-
const size = isCode
|
|
60
|
-
? lineCount! * 40
|
|
61
|
-
: Math.floor(Math.random() * 200000) + 1000;
|
|
58
|
+
const size = isCode ? lineCount! * 40 : Math.floor(Math.random() * 200000) + 1000;
|
|
62
59
|
|
|
63
60
|
buildings.push({
|
|
64
61
|
path: `${basePath}/file${i}.${ext}`,
|
|
@@ -92,28 +89,44 @@ const sampleCityData: CityData = {
|
|
|
92
89
|
worldBounds: { minX: -2, maxX: 42, minZ: -2, maxZ: 42 },
|
|
93
90
|
fileCount: 12,
|
|
94
91
|
type: 'directory',
|
|
95
|
-
label: {
|
|
92
|
+
label: {
|
|
93
|
+
text: 'src',
|
|
94
|
+
bounds: { minX: -2, maxX: 42, minZ: 42, maxZ: 46 },
|
|
95
|
+
position: 'bottom',
|
|
96
|
+
},
|
|
96
97
|
},
|
|
97
98
|
{
|
|
98
99
|
path: 'src/components',
|
|
99
100
|
worldBounds: { minX: 48, maxX: 82, minZ: -2, maxZ: 32 },
|
|
100
101
|
fileCount: 8,
|
|
101
102
|
type: 'directory',
|
|
102
|
-
label: {
|
|
103
|
+
label: {
|
|
104
|
+
text: 'components',
|
|
105
|
+
bounds: { minX: 48, maxX: 82, minZ: 32, maxZ: 36 },
|
|
106
|
+
position: 'bottom',
|
|
107
|
+
},
|
|
103
108
|
},
|
|
104
109
|
{
|
|
105
110
|
path: 'src/utils',
|
|
106
111
|
worldBounds: { minX: 48, maxX: 77, minZ: 38, maxZ: 67 },
|
|
107
112
|
fileCount: 6,
|
|
108
113
|
type: 'directory',
|
|
109
|
-
label: {
|
|
114
|
+
label: {
|
|
115
|
+
text: 'utils',
|
|
116
|
+
bounds: { minX: 48, maxX: 77, minZ: 67, maxZ: 71 },
|
|
117
|
+
position: 'bottom',
|
|
118
|
+
},
|
|
110
119
|
},
|
|
111
120
|
{
|
|
112
121
|
path: 'tests',
|
|
113
122
|
worldBounds: { minX: -2, maxX: 32, minZ: 48, maxZ: 72 },
|
|
114
123
|
fileCount: 5,
|
|
115
124
|
type: 'directory',
|
|
116
|
-
label: {
|
|
125
|
+
label: {
|
|
126
|
+
text: 'tests',
|
|
127
|
+
bounds: { minX: -2, maxX: 32, minZ: 72, maxZ: 76 },
|
|
128
|
+
position: 'bottom',
|
|
129
|
+
},
|
|
117
130
|
},
|
|
118
131
|
],
|
|
119
132
|
bounds: { minX: -5, maxX: 85, minZ: -5, maxZ: 80 },
|
|
@@ -164,7 +177,11 @@ function generateLargeCityData(): CityData {
|
|
|
164
177
|
buildings,
|
|
165
178
|
districts,
|
|
166
179
|
bounds: { minX: -10, maxX: totalSize + 10, minZ: -10, maxZ: totalSize + 10 },
|
|
167
|
-
metadata: {
|
|
180
|
+
metadata: {
|
|
181
|
+
totalFiles: buildings.length,
|
|
182
|
+
totalDirectories: districts.length,
|
|
183
|
+
rootPath: '/large-project',
|
|
184
|
+
},
|
|
168
185
|
};
|
|
169
186
|
}
|
|
170
187
|
|
|
@@ -211,7 +228,11 @@ function generateMonorepoCityData(): CityData {
|
|
|
211
228
|
buildings,
|
|
212
229
|
districts,
|
|
213
230
|
bounds: { minX: -10, maxX: 175, minZ: -10, maxZ: 110 },
|
|
214
|
-
metadata: {
|
|
231
|
+
metadata: {
|
|
232
|
+
totalFiles: buildings.length,
|
|
233
|
+
totalDirectories: districts.length,
|
|
234
|
+
rootPath: '/monorepo',
|
|
235
|
+
},
|
|
215
236
|
};
|
|
216
237
|
}
|
|
217
238
|
|
|
@@ -345,7 +366,7 @@ export const WithClickHandler: Story = {
|
|
|
345
366
|
args: {
|
|
346
367
|
cityData: sampleCityData,
|
|
347
368
|
height: '100vh',
|
|
348
|
-
onBuildingClick:
|
|
369
|
+
onBuildingClick: building => {
|
|
349
370
|
console.log('Clicked building:', building.path);
|
|
350
371
|
alert(`Clicked: ${building.path}`);
|
|
351
372
|
},
|
|
@@ -497,14 +518,16 @@ const authServerTourSteps: TourStep[] = [
|
|
|
497
518
|
{
|
|
498
519
|
id: 'overview',
|
|
499
520
|
title: 'Welcome to Auth Server',
|
|
500
|
-
description:
|
|
521
|
+
description:
|
|
522
|
+
"This is the authentication server for Principal ADE. Let's explore its architecture.",
|
|
501
523
|
highlightLayers: [],
|
|
502
524
|
isolationMode: 'none' as const,
|
|
503
525
|
},
|
|
504
526
|
{
|
|
505
527
|
id: 'workos-auth',
|
|
506
528
|
title: 'WorkOS Authentication',
|
|
507
|
-
description:
|
|
529
|
+
description:
|
|
530
|
+
'The core authentication flow using WorkOS. Handles OAuth callbacks, token exchange, and verification.',
|
|
508
531
|
highlightLayers: [
|
|
509
532
|
{
|
|
510
533
|
id: 'workos',
|
|
@@ -656,7 +679,9 @@ const AuthServerTourTemplate: React.FC = () => {
|
|
|
656
679
|
};
|
|
657
680
|
|
|
658
681
|
return (
|
|
659
|
-
<div
|
|
682
|
+
<div
|
|
683
|
+
style={{ height: '100vh', display: 'flex', flexDirection: 'column', position: 'relative' }}
|
|
684
|
+
>
|
|
660
685
|
{/* 3D City */}
|
|
661
686
|
<FileCity3D
|
|
662
687
|
cityData={authServerCityData as CityData}
|
|
@@ -713,7 +738,15 @@ const AuthServerTourTemplate: React.FC = () => {
|
|
|
713
738
|
</button>
|
|
714
739
|
|
|
715
740
|
{/* Step content - center */}
|
|
716
|
-
<div
|
|
741
|
+
<div
|
|
742
|
+
style={{
|
|
743
|
+
flex: 1,
|
|
744
|
+
display: 'flex',
|
|
745
|
+
flexDirection: 'column',
|
|
746
|
+
alignItems: 'center',
|
|
747
|
+
gap: 8,
|
|
748
|
+
}}
|
|
749
|
+
>
|
|
717
750
|
{/* Step indicators */}
|
|
718
751
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
719
752
|
{authServerTourSteps.map((s, i) => (
|
|
@@ -725,7 +758,8 @@ const AuthServerTourTemplate: React.FC = () => {
|
|
|
725
758
|
height: i === currentStep ? 12 : 10,
|
|
726
759
|
borderRadius: '50%',
|
|
727
760
|
border: i === currentStep ? '2px solid #3b82f6' : 'none',
|
|
728
|
-
background:
|
|
761
|
+
background:
|
|
762
|
+
i === currentStep ? '#3b82f6' : i < currentStep ? '#22c55e' : '#475569',
|
|
729
763
|
cursor: 'pointer',
|
|
730
764
|
padding: 0,
|
|
731
765
|
transition: 'all 0.2s',
|
|
@@ -745,16 +779,35 @@ const AuthServerTourTemplate: React.FC = () => {
|
|
|
745
779
|
</div>
|
|
746
780
|
|
|
747
781
|
{/* Description */}
|
|
748
|
-
<p
|
|
782
|
+
<p
|
|
783
|
+
style={{
|
|
784
|
+
margin: 0,
|
|
785
|
+
fontSize: 13,
|
|
786
|
+
color: '#94a3b8',
|
|
787
|
+
textAlign: 'center',
|
|
788
|
+
maxWidth: 600,
|
|
789
|
+
}}
|
|
790
|
+
>
|
|
749
791
|
{step.description}
|
|
750
792
|
</p>
|
|
751
793
|
|
|
752
794
|
{/* Isolation mode indicator */}
|
|
753
795
|
<div style={{ fontSize: 11, color: '#64748b' }}>
|
|
754
|
-
Isolation:
|
|
796
|
+
Isolation:{' '}
|
|
797
|
+
<code
|
|
798
|
+
style={{
|
|
799
|
+
color: '#94a3b8',
|
|
800
|
+
background: '#1e293b',
|
|
801
|
+
padding: '2px 6px',
|
|
802
|
+
borderRadius: 4,
|
|
803
|
+
}}
|
|
804
|
+
>
|
|
805
|
+
{step.isolationMode}
|
|
806
|
+
</code>
|
|
755
807
|
{step.highlightLayers.length > 0 && (
|
|
756
808
|
<span style={{ marginLeft: 8 }}>
|
|
757
|
-
• {step.highlightLayers.length} layer{step.highlightLayers.length > 1 ? 's' : ''}
|
|
809
|
+
• {step.highlightLayers.length} layer{step.highlightLayers.length > 1 ? 's' : ''}{' '}
|
|
810
|
+
active
|
|
758
811
|
</span>
|
|
759
812
|
)}
|
|
760
813
|
</div>
|
|
@@ -806,7 +859,9 @@ const DirectorySelectionTemplate: React.FC = () => {
|
|
|
806
859
|
}, []);
|
|
807
860
|
|
|
808
861
|
return (
|
|
809
|
-
<div
|
|
862
|
+
<div
|
|
863
|
+
style={{ height: '100vh', display: 'flex', flexDirection: 'column', position: 'relative' }}
|
|
864
|
+
>
|
|
810
865
|
{/* 3D City */}
|
|
811
866
|
<FileCity3D
|
|
812
867
|
cityData={authServerCityData as CityData}
|
|
@@ -822,12 +877,12 @@ const DirectorySelectionTemplate: React.FC = () => {
|
|
|
822
877
|
friction: 14,
|
|
823
878
|
}}
|
|
824
879
|
showControls={true}
|
|
825
|
-
onBuildingClick={
|
|
880
|
+
onBuildingClick={building => {
|
|
826
881
|
// Extract directory from building path
|
|
827
882
|
const parts = building.path.split('/');
|
|
828
883
|
if (parts.length >= 2) {
|
|
829
884
|
const dir = parts.slice(0, 2).join('/');
|
|
830
|
-
setFocusDirectory(prev => prev === dir ? null : dir);
|
|
885
|
+
setFocusDirectory(prev => (prev === dir ? null : dir));
|
|
831
886
|
}
|
|
832
887
|
}}
|
|
833
888
|
/>
|
|
@@ -869,7 +924,7 @@ const DirectorySelectionTemplate: React.FC = () => {
|
|
|
869
924
|
{directories.map(dir => (
|
|
870
925
|
<button
|
|
871
926
|
key={dir}
|
|
872
|
-
onClick={() => setFocusDirectory(prev => prev === dir ? null : dir)}
|
|
927
|
+
onClick={() => setFocusDirectory(prev => (prev === dir ? null : dir))}
|
|
873
928
|
style={{
|
|
874
929
|
padding: '8px 16px',
|
|
875
930
|
background: focusDirectory === dir ? '#3b82f6' : '#334155',
|
|
@@ -887,7 +942,17 @@ const DirectorySelectionTemplate: React.FC = () => {
|
|
|
887
942
|
</div>
|
|
888
943
|
{focusDirectory && (
|
|
889
944
|
<div style={{ marginTop: 12, fontSize: 14 }}>
|
|
890
|
-
Focused:
|
|
945
|
+
Focused:{' '}
|
|
946
|
+
<code
|
|
947
|
+
style={{
|
|
948
|
+
color: '#3b82f6',
|
|
949
|
+
background: '#1e293b',
|
|
950
|
+
padding: '4px 8px',
|
|
951
|
+
borderRadius: 4,
|
|
952
|
+
}}
|
|
953
|
+
>
|
|
954
|
+
{focusDirectory}
|
|
955
|
+
</code>
|
|
891
956
|
</div>
|
|
892
957
|
)}
|
|
893
958
|
</div>
|