@principal-ai/file-city-react 0.5.9 → 0.5.11
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 +7 -1
- package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
- package/dist/components/FileCity3D/FileCity3D.js +80 -38
- package/package.json +1 -3
- package/src/components/FileCity3D/FileCity3D.tsx +184 -151
- package/src/stories/FileCity3D.stories.tsx +120 -33
|
@@ -96,6 +96,12 @@ 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;
|
|
103
|
+
/** Currently selected building (controlled by host) */
|
|
104
|
+
selectedBuilding?: CityBuilding | null;
|
|
99
105
|
}
|
|
100
106
|
/**
|
|
101
107
|
* FileCity3D - 3D visualization of codebase structure
|
|
@@ -103,6 +109,6 @@ export interface FileCity3DProps {
|
|
|
103
109
|
* Renders CityData as an interactive 3D city where buildings represent files
|
|
104
110
|
* and their height corresponds to line count or file size.
|
|
105
111
|
*/
|
|
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;
|
|
112
|
+
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, selectedBuilding, }: FileCity3DProps): import("react/jsx-runtime").JSX.Element;
|
|
107
113
|
export default FileCity3D;
|
|
108
114
|
//# 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;
|
|
@@ -261,7 +259,7 @@ function isPathInDirectory(path, directory) {
|
|
|
261
259
|
return true;
|
|
262
260
|
return path === directory || path.startsWith(directory + '/');
|
|
263
261
|
}
|
|
264
|
-
function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hoveredIndex, growProgress, animationConfig, heightScaling, linearScale, staggerIndices, focusDirectory, highlightLayers, isolationMode, }) {
|
|
262
|
+
function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hoveredIndex, selectedIndex, growProgress, animationConfig, heightScaling, linearScale, staggerIndices, focusDirectory, highlightLayers, isolationMode, }) {
|
|
265
263
|
const meshRef = useRef(null);
|
|
266
264
|
const startTimeRef = useRef(null);
|
|
267
265
|
const tempObject = useMemo(() => new THREE.Object3D(), []);
|
|
@@ -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;
|
|
@@ -389,7 +395,8 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
389
395
|
const height = animProgress * fullHeight * newMultiplier + minHeight;
|
|
390
396
|
const yPosition = height / 2 + baseOffset;
|
|
391
397
|
const isHovered = hoveredIndex === data.index;
|
|
392
|
-
const
|
|
398
|
+
const isSelected = selectedIndex === data.index;
|
|
399
|
+
const scale = isSelected ? 1.08 : isHovered ? 1.05 : 1;
|
|
393
400
|
tempObject.position.set(x, yPosition, z);
|
|
394
401
|
tempObject.scale.set(width * scale, height, depth * scale);
|
|
395
402
|
tempObject.updateMatrix();
|
|
@@ -404,7 +411,10 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
404
411
|
tempColor.g = tempColor.g * (1 - grayAmount) + gray * grayAmount;
|
|
405
412
|
tempColor.b = tempColor.b * (1 - grayAmount) + gray * grayAmount;
|
|
406
413
|
}
|
|
407
|
-
if (
|
|
414
|
+
if (isSelected) {
|
|
415
|
+
tempColor.multiplyScalar(1.4);
|
|
416
|
+
}
|
|
417
|
+
else if (isHovered) {
|
|
408
418
|
tempColor.multiplyScalar(1.2);
|
|
409
419
|
}
|
|
410
420
|
meshRef.current.setColorAt(instanceIndex, tempColor);
|
|
@@ -413,6 +423,8 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
413
423
|
if (meshRef.current.instanceColor) {
|
|
414
424
|
meshRef.current.instanceColor.needsUpdate = true;
|
|
415
425
|
}
|
|
426
|
+
// Update bounding sphere for raycasting as buildings grow/animate
|
|
427
|
+
meshRef.current.computeBoundingSphere();
|
|
416
428
|
});
|
|
417
429
|
const handlePointerMove = useCallback((e) => {
|
|
418
430
|
e.stopPropagation();
|
|
@@ -476,7 +488,7 @@ function AnimatedIcon({ x, z, targetHeight, iconSize, texture, opacity, growProg
|
|
|
476
488
|
materialRef.current.opacity = opacity * animProgress;
|
|
477
489
|
}
|
|
478
490
|
});
|
|
479
|
-
return (_jsx("sprite", { ref: spriteRef, position: [x, 0, z], scale: [iconSize, iconSize, 1], children: _jsx("spriteMaterial", { ref: materialRef, map: texture, transparent: true, opacity: 0, depthTest: true, depthWrite: false }) }));
|
|
491
|
+
return (_jsx("sprite", { ref: spriteRef, position: [x, 0, z], scale: [iconSize, iconSize, 1], raycast: () => null, children: _jsx("spriteMaterial", { ref: materialRef, map: texture, transparent: true, opacity: 0, depthTest: true, depthWrite: false }) }));
|
|
480
492
|
}
|
|
481
493
|
function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, linearScale, highlightLayers, isolationMode, hasActiveHighlights, staggerIndices, springDuration, staggerDelay, }) {
|
|
482
494
|
// Pre-compute buildings with icons
|
|
@@ -510,7 +522,17 @@ function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, l
|
|
|
510
522
|
};
|
|
511
523
|
})
|
|
512
524
|
.filter(Boolean);
|
|
513
|
-
}, [
|
|
525
|
+
}, [
|
|
526
|
+
buildings,
|
|
527
|
+
centerOffset,
|
|
528
|
+
highlightLayers,
|
|
529
|
+
isolationMode,
|
|
530
|
+
hasActiveHighlights,
|
|
531
|
+
heightScaling,
|
|
532
|
+
linearScale,
|
|
533
|
+
staggerIndices,
|
|
534
|
+
staggerDelay,
|
|
535
|
+
]);
|
|
514
536
|
// Don't render if no progress yet
|
|
515
537
|
if (growProgress < 0.1)
|
|
516
538
|
return null;
|
|
@@ -528,7 +550,7 @@ function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, l
|
|
|
528
550
|
return (_jsx(AnimatedIcon, { x: x, z: z, targetHeight: targetHeight, iconSize: iconSize, texture: texture, opacity: opacity, growProgress: growProgress, staggerDelayMs: staggerDelayMs, springDuration: springDuration }, building.path));
|
|
529
551
|
}) }));
|
|
530
552
|
}
|
|
531
|
-
function DistrictFloor({ district, centerOffset, opacity
|
|
553
|
+
function DistrictFloor({ district, centerOffset, opacity }) {
|
|
532
554
|
const { worldBounds } = district;
|
|
533
555
|
const width = worldBounds.maxX - worldBounds.minX;
|
|
534
556
|
const depth = worldBounds.maxZ - worldBounds.minZ;
|
|
@@ -547,6 +569,7 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }) {
|
|
|
547
569
|
const { camera } = useThree();
|
|
548
570
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
549
571
|
const controlsRef = useRef(null);
|
|
572
|
+
const isAnimatingRef = useRef(true);
|
|
550
573
|
// Animated camera position and target
|
|
551
574
|
const targetPos = useMemo(() => {
|
|
552
575
|
if (focusTarget) {
|
|
@@ -583,10 +606,17 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }) {
|
|
|
583
606
|
lookY: targetPos.targetY,
|
|
584
607
|
lookZ: targetPos.targetZ,
|
|
585
608
|
config: { tension: 60, friction: 20 },
|
|
609
|
+
onStart: () => {
|
|
610
|
+
isAnimatingRef.current = true;
|
|
611
|
+
},
|
|
612
|
+
onRest: () => {
|
|
613
|
+
isAnimatingRef.current = false;
|
|
614
|
+
},
|
|
586
615
|
});
|
|
587
|
-
// Update camera each frame
|
|
616
|
+
// Update camera each frame only while spring is animating
|
|
617
|
+
// Once animation settles, let OrbitControls handle user interaction
|
|
588
618
|
useFrame(() => {
|
|
589
|
-
if (!controlsRef.current)
|
|
619
|
+
if (!controlsRef.current || !isAnimatingRef.current)
|
|
590
620
|
return;
|
|
591
621
|
camera.position.set(camX.get(), camY.get(), camZ.get());
|
|
592
622
|
controlsRef.current.target.set(lookX.get(), lookY.get(), lookZ.get());
|
|
@@ -642,9 +672,9 @@ function InfoPanel({ building }) {
|
|
|
642
672
|
marginTop: 4,
|
|
643
673
|
display: 'flex',
|
|
644
674
|
gap: 12,
|
|
645
|
-
}, children: [building.lineCount !== undefined && (_jsxs("span", { children: [building.lineCount.toLocaleString(), " lines"] })), building.size !== undefined &&
|
|
675
|
+
}, children: [building.lineCount !== undefined && (_jsxs("span", { children: [building.lineCount.toLocaleString(), " lines"] })), building.size !== undefined && _jsxs("span", { children: [(building.size / 1024).toFixed(1), " KB"] })] })] }));
|
|
646
676
|
}
|
|
647
|
-
function ControlsOverlay({ isFlat, onToggle, onResetCamera
|
|
677
|
+
function ControlsOverlay({ isFlat, onToggle, onResetCamera }) {
|
|
648
678
|
const buttonStyle = {
|
|
649
679
|
background: 'rgba(15, 23, 42, 0.9)',
|
|
650
680
|
border: '1px solid #334155',
|
|
@@ -665,7 +695,7 @@ function ControlsOverlay({ isFlat, onToggle, onResetCamera, }) {
|
|
|
665
695
|
gap: 8,
|
|
666
696
|
}, children: [_jsx("button", { onClick: onResetCamera, style: buttonStyle, children: "Reset View" }), _jsx("button", { onClick: onToggle, style: buttonStyle, children: isFlat ? 'Grow to 3D' : 'Flatten to 2D' })] }));
|
|
667
697
|
}
|
|
668
|
-
function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding, growProgress, animationConfig, highlightLayers, isolationMode, heightScaling, linearScale, focusDirectory, }) {
|
|
698
|
+
function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding, selectedBuilding, growProgress, animationConfig, highlightLayers, isolationMode, heightScaling, linearScale, focusDirectory, }) {
|
|
669
699
|
const centerOffset = useMemo(() => ({
|
|
670
700
|
x: (cityData.bounds.minX + cityData.bounds.maxX) / 2,
|
|
671
701
|
z: (cityData.bounds.minZ + cityData.bounds.maxZ) / 2,
|
|
@@ -747,7 +777,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
747
777
|
const focusTarget = useMemo(() => {
|
|
748
778
|
// Use camera focus directory for camera movement
|
|
749
779
|
if (cameraFocusDirectory) {
|
|
750
|
-
const focusedBuildings = cityData.buildings.filter(
|
|
780
|
+
const focusedBuildings = cityData.buildings.filter(building => isPathInDirectory(building.path, cameraFocusDirectory));
|
|
751
781
|
if (focusedBuildings.length === 0)
|
|
752
782
|
return null;
|
|
753
783
|
let minX = Infinity, maxX = -Infinity;
|
|
@@ -766,10 +796,11 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
766
796
|
const size = Math.max(maxX - minX, maxZ - minZ);
|
|
767
797
|
return { x: centerX, z: centerZ, size };
|
|
768
798
|
}
|
|
769
|
-
// Priority 2: highlight layers
|
|
770
|
-
if
|
|
799
|
+
// Priority 2: highlight layers (only if no focusDirectory is pending)
|
|
800
|
+
// Don't focus on highlights if we're waiting for cameraFocusDirectory to catch up
|
|
801
|
+
if (!activeHighlights || focusDirectory)
|
|
771
802
|
return null;
|
|
772
|
-
const highlightedBuildings = cityData.buildings.filter(
|
|
803
|
+
const highlightedBuildings = cityData.buildings.filter(building => {
|
|
773
804
|
const highlight = getHighlightForPath(building.path, highlightLayers);
|
|
774
805
|
return highlight !== null;
|
|
775
806
|
});
|
|
@@ -790,14 +821,21 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
790
821
|
const centerZ = (minZ + maxZ) / 2;
|
|
791
822
|
const size = Math.max(maxX - minX, maxZ - minZ);
|
|
792
823
|
return { x: centerX, z: centerZ, size };
|
|
793
|
-
}, [
|
|
824
|
+
}, [
|
|
825
|
+
cameraFocusDirectory,
|
|
826
|
+
focusDirectory,
|
|
827
|
+
activeHighlights,
|
|
828
|
+
cityData.buildings,
|
|
829
|
+
highlightLayers,
|
|
830
|
+
centerOffset,
|
|
831
|
+
isPathInDirectory,
|
|
832
|
+
]);
|
|
794
833
|
const staggerIndices = useMemo(() => {
|
|
795
834
|
const centerX = (cityData.bounds.minX + cityData.bounds.maxX) / 2;
|
|
796
835
|
const centerZ = (cityData.bounds.minZ + cityData.bounds.maxZ) / 2;
|
|
797
836
|
const withDistance = cityData.buildings.map((b, originalIndex) => ({
|
|
798
837
|
originalIndex,
|
|
799
|
-
distance: Math.sqrt(Math.pow(b.position.x - centerX, 2) +
|
|
800
|
-
Math.pow(b.position.z - centerZ, 2)),
|
|
838
|
+
distance: Math.sqrt(Math.pow(b.position.x - centerX, 2) + Math.pow(b.position.z - centerZ, 2)),
|
|
801
839
|
}));
|
|
802
840
|
withDistance.sort((a, b) => a.distance - b.distance);
|
|
803
841
|
const indices = new Array(cityData.buildings.length);
|
|
@@ -809,13 +847,18 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
809
847
|
const hoveredIndex = useMemo(() => {
|
|
810
848
|
if (!hoveredBuilding)
|
|
811
849
|
return null;
|
|
812
|
-
return cityData.buildings.findIndex(
|
|
850
|
+
return cityData.buildings.findIndex(b => b.path === hoveredBuilding.path);
|
|
813
851
|
}, [hoveredBuilding, cityData.buildings]);
|
|
852
|
+
const selectedIndex = useMemo(() => {
|
|
853
|
+
if (!selectedBuilding)
|
|
854
|
+
return null;
|
|
855
|
+
return cityData.buildings.findIndex(b => b.path === selectedBuilding.path);
|
|
856
|
+
}, [selectedBuilding, cityData.buildings]);
|
|
814
857
|
// Calculate spring duration for animation sync
|
|
815
858
|
const tension = animationConfig.tension || 120;
|
|
816
859
|
const friction = animationConfig.friction || 14;
|
|
817
860
|
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(
|
|
861
|
+
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, selectedIndex: selectedIndex, 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
862
|
}
|
|
820
863
|
/**
|
|
821
864
|
* FileCity3D - 3D visualization of codebase structure
|
|
@@ -823,8 +866,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
823
866
|
* Renders CityData as an interactive 3D city where buildings represent files
|
|
824
867
|
* and their height corresponds to line count or file size.
|
|
825
868
|
*/
|
|
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();
|
|
869
|
+
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', selectedBuilding = null, }) {
|
|
828
870
|
const [hoveredBuilding, setHoveredBuilding] = useState(null);
|
|
829
871
|
const [internalIsGrown, setInternalIsGrown] = useState(false);
|
|
830
872
|
const animationConfig = useMemo(() => ({ ...DEFAULT_ANIMATION, ...animation }), [animation]);
|
|
@@ -854,12 +896,12 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
854
896
|
width,
|
|
855
897
|
height,
|
|
856
898
|
position: 'relative',
|
|
857
|
-
background:
|
|
899
|
+
background: backgroundColor,
|
|
858
900
|
overflow: 'hidden',
|
|
859
901
|
display: 'flex',
|
|
860
902
|
alignItems: 'center',
|
|
861
903
|
justifyContent: 'center',
|
|
862
|
-
color:
|
|
904
|
+
color: textColor,
|
|
863
905
|
fontFamily: 'system-ui, sans-serif',
|
|
864
906
|
fontSize: 14,
|
|
865
907
|
...style,
|
|
@@ -870,12 +912,12 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
870
912
|
width,
|
|
871
913
|
height,
|
|
872
914
|
position: 'relative',
|
|
873
|
-
background:
|
|
915
|
+
background: backgroundColor,
|
|
874
916
|
overflow: 'hidden',
|
|
875
917
|
display: 'flex',
|
|
876
918
|
alignItems: 'center',
|
|
877
919
|
justifyContent: 'center',
|
|
878
|
-
color:
|
|
920
|
+
color: textColor,
|
|
879
921
|
fontFamily: 'system-ui, sans-serif',
|
|
880
922
|
fontSize: 14,
|
|
881
923
|
...style,
|
|
@@ -885,7 +927,7 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
885
927
|
width,
|
|
886
928
|
height,
|
|
887
929
|
position: 'relative',
|
|
888
|
-
background:
|
|
930
|
+
background: backgroundColor,
|
|
889
931
|
overflow: 'hidden',
|
|
890
932
|
...style,
|
|
891
933
|
}, children: [_jsx(Canvas, { shadows: true, style: {
|
|
@@ -894,6 +936,6 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
894
936
|
left: 0,
|
|
895
937
|
width: '100%',
|
|
896
938
|
height: '100%',
|
|
897
|
-
}, children: _jsx(CityScene, { cityData: cityData, onBuildingHover: setHoveredBuilding, onBuildingClick: onBuildingClick, hoveredBuilding: hoveredBuilding, growProgress: growProgress, animationConfig: animationConfig, highlightLayers: highlightLayers, isolationMode: isolationMode, heightScaling: heightScaling, linearScale: linearScale, focusDirectory: focusDirectory }) }), _jsx(InfoPanel, { building: hoveredBuilding }), showControls && (_jsx(ControlsOverlay, { isFlat: !isGrown, onToggle: handleToggle, onResetCamera: resetCamera }))] }));
|
|
939
|
+
}, children: _jsx(CityScene, { cityData: cityData, onBuildingHover: setHoveredBuilding, onBuildingClick: onBuildingClick, hoveredBuilding: hoveredBuilding, selectedBuilding: selectedBuilding, growProgress: growProgress, animationConfig: animationConfig, highlightLayers: highlightLayers, isolationMode: isolationMode, heightScaling: heightScaling, linearScale: linearScale, focusDirectory: focusDirectory }) }), _jsx(InfoPanel, { building: selectedBuilding || hoveredBuilding }), showControls && (_jsx(ControlsOverlay, { isFlat: !isGrown, onToggle: handleToggle, onResetCamera: resetCamera }))] }));
|
|
898
940
|
}
|
|
899
941
|
export default FileCity3D;
|
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.11",
|
|
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",
|