@principal-ai/file-city-react 0.5.8 → 0.5.9
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 +283 -70
- package/package.json +1 -1
- package/src/components/FileCity3D/FileCity3D.tsx +348 -103
- package/src/stories/FileCity3D.stories.tsx +359 -1
|
@@ -92,6 +92,10 @@ export interface FileCity3DProps {
|
|
|
92
92
|
heightScaling?: HeightScaling;
|
|
93
93
|
/** Scale factor for linear mode (height per line, default 0.05) */
|
|
94
94
|
linearScale?: number;
|
|
95
|
+
/** Directory path to focus on - buildings outside will collapse */
|
|
96
|
+
focusDirectory?: string | null;
|
|
97
|
+
/** Callback when user clicks on a district to navigate */
|
|
98
|
+
onDirectorySelect?: (directory: string | null) => void;
|
|
95
99
|
}
|
|
96
100
|
/**
|
|
97
101
|
* FileCity3D - 3D visualization of codebase structure
|
|
@@ -99,6 +103,6 @@ export interface FileCity3DProps {
|
|
|
99
103
|
* Renders CityData as an interactive 3D city where buildings represent files
|
|
100
104
|
* and their height corresponds to line count or file size.
|
|
101
105
|
*/
|
|
102
|
-
export declare function FileCity3D({ cityData, width, height, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls, highlightLayers, isolationMode, dimOpacity, isLoading, loadingMessage, emptyMessage, heightScaling, linearScale, }: FileCity3DProps): import("react/jsx-runtime").JSX.Element;
|
|
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;
|
|
103
107
|
export default FileCity3D;
|
|
104
108
|
//# 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,KAMN,MAAM,OAAO,CAAC;AAWf,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAEb,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxD,OAAO,QAAQ,OAAO,CAAC;IAErB,UAAU,GAAG,CAAC;QAEZ,UAAU,iBAAkB,SAAQ,aAAa;SAAG;KACrD;CACF;AAGD,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC;AAGrD,MAAM,WAAW,cAAc;IAC7B,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;CAC5B;AAED,gDAAgD;AAChD,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,aAAa,GACb,UAAU,GACV,MAAM,CAAC;AAGX,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wCAAwC;AACxC,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAMN,MAAM,OAAO,CAAC;AAWf,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAEb,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxD,OAAO,QAAQ,OAAO,CAAC;IAErB,UAAU,GAAG,CAAC;QAEZ,UAAU,iBAAkB,SAAQ,aAAa;SAAG;KACrD;CACF;AAGD,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC;AAGrD,MAAM,WAAW,cAAc;IAC7B,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;CAC5B;AAED,gDAAgD;AAChD,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,aAAa,GACb,UAAU,GACV,MAAM,CAAC;AAGX,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wCAAwC;AACxC,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,QAAQ,CAAC;AA02BrD,wBAAgB,WAAW,SAE1B;AAmeD,MAAM,WAAW,eAAe;IAC9B,uCAAuC;IACvC,QAAQ,EAAE,QAAQ,CAAC;IACnB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,0CAA0C;IAC1C,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;IACnD,qBAAqB;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,8BAA8B;IAC9B,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,uCAAuC;IACvC,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC1C,2BAA2B;IAC3B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kEAAkE;IAClE,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;IACnC,yEAAyE;IACzE,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uCAAuC;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,0DAA0D;IAC1D,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CACxD;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,KAAc,EACd,MAAY,EACZ,eAAe,EACf,SAAS,EACT,KAAK,EACL,SAAS,EACT,OAAO,EAAE,eAAe,EACxB,YAAY,EACZ,YAAmB,EACnB,eAAoB,EACpB,aAA6B,EAC7B,UAAiB,EACjB,SAAiB,EACjB,cAAuC,EACvC,YAA4C,EAC5C,aAA6B,EAC7B,WAAkB,EAClB,cAAqB,EACrB,iBAAiB,GAClB,EAAE,eAAe,2CAmIjB;AAED,eAAe,UAAU,CAAC"}
|
|
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
10
10
|
import { useMemo, useRef, useState, useEffect, useCallback, } from 'react';
|
|
11
11
|
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
|
12
12
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
13
|
-
import { animated } from '@react-spring/three';
|
|
13
|
+
import { animated, useSpring } from '@react-spring/three';
|
|
14
14
|
import { OrbitControls, PerspectiveCamera, Text, RoundedBox, } from '@react-three/drei';
|
|
15
15
|
import { getFileConfig } from '@principal-ai/file-city-builder';
|
|
16
16
|
import * as THREE from 'three';
|
|
@@ -195,7 +195,9 @@ function hasActiveHighlights(layers) {
|
|
|
195
195
|
}
|
|
196
196
|
// Animated RoundedBox wrapper
|
|
197
197
|
const AnimatedRoundedBox = animated(RoundedBox);
|
|
198
|
-
|
|
198
|
+
// Animated meshStandardMaterial for opacity transitions
|
|
199
|
+
const AnimatedMeshStandardMaterial = animated('meshStandardMaterial');
|
|
200
|
+
function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springDuration, heightMultipliersRef }) {
|
|
199
201
|
const meshRef = useRef(null);
|
|
200
202
|
const startTimeRef = useRef(null);
|
|
201
203
|
const tempObject = useMemo(() => new THREE.Object3D(), []);
|
|
@@ -204,14 +206,14 @@ function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springD
|
|
|
204
206
|
// Pre-compute edge data
|
|
205
207
|
const edgeData = useMemo(() => {
|
|
206
208
|
return buildings.flatMap((data) => {
|
|
207
|
-
const { width, depth, x, z,
|
|
209
|
+
const { width, depth, x, z, fullHeight, staggerDelayMs, buildingIndex } = data;
|
|
208
210
|
const halfW = width / 2;
|
|
209
211
|
const halfD = depth / 2;
|
|
210
212
|
return [
|
|
211
|
-
{ x: x - halfW, z: z - halfD,
|
|
212
|
-
{ x: x + halfW, z: z - halfD,
|
|
213
|
-
{ x: x - halfW, z: z + halfD,
|
|
214
|
-
{ x: x + halfW, z: z + halfD,
|
|
213
|
+
{ x: x - halfW, z: z - halfD, fullHeight, staggerDelayMs, buildingIndex },
|
|
214
|
+
{ x: x + halfW, z: z - halfD, fullHeight, staggerDelayMs, buildingIndex },
|
|
215
|
+
{ x: x - halfW, z: z + halfD, fullHeight, staggerDelayMs, buildingIndex },
|
|
216
|
+
{ x: x + halfW, z: z + halfD, fullHeight, staggerDelayMs, buildingIndex },
|
|
215
217
|
];
|
|
216
218
|
});
|
|
217
219
|
}, [buildings]);
|
|
@@ -225,7 +227,9 @@ function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springD
|
|
|
225
227
|
const currentTime = clock.elapsedTime * 1000;
|
|
226
228
|
const animStartTime = startTimeRef.current ?? currentTime;
|
|
227
229
|
edgeData.forEach((edge, idx) => {
|
|
228
|
-
const { x, z,
|
|
230
|
+
const { x, z, fullHeight, staggerDelayMs, buildingIndex } = edge;
|
|
231
|
+
// Get height multiplier from shared ref (for collapse animation)
|
|
232
|
+
const heightMultiplier = heightMultipliersRef.current?.[buildingIndex] ?? 1;
|
|
229
233
|
// Calculate per-building animation progress
|
|
230
234
|
const elapsed = currentTime - animStartTime - staggerDelayMs;
|
|
231
235
|
let animProgress = growProgress;
|
|
@@ -237,7 +241,8 @@ function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springD
|
|
|
237
241
|
else if (growProgress > 0 && elapsed < 0) {
|
|
238
242
|
animProgress = 0;
|
|
239
243
|
}
|
|
240
|
-
|
|
244
|
+
// Apply both grow animation and collapse multiplier
|
|
245
|
+
const height = animProgress * fullHeight * heightMultiplier + minHeight;
|
|
241
246
|
const yPosition = height / 2 + baseOffset;
|
|
242
247
|
tempObject.position.set(x, yPosition, z);
|
|
243
248
|
tempObject.scale.set(0.3, height, 0.3); // Thin box for edge
|
|
@@ -250,24 +255,58 @@ function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springD
|
|
|
250
255
|
return null;
|
|
251
256
|
return (_jsxs("instancedMesh", { ref: meshRef, args: [undefined, undefined, numEdges], frustumCulled: false, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshBasicMaterial", { color: "#1a1a2e", transparent: true, opacity: 0.7 })] }));
|
|
252
257
|
}
|
|
253
|
-
|
|
258
|
+
// Helper to check if a path is inside a directory
|
|
259
|
+
function isPathInDirectory(path, directory) {
|
|
260
|
+
if (!directory)
|
|
261
|
+
return true;
|
|
262
|
+
return path === directory || path.startsWith(directory + '/');
|
|
263
|
+
}
|
|
264
|
+
function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hoveredIndex, growProgress, animationConfig, heightScaling, linearScale, staggerIndices, focusDirectory, highlightLayers, isolationMode, }) {
|
|
254
265
|
const meshRef = useRef(null);
|
|
255
266
|
const startTimeRef = useRef(null);
|
|
256
267
|
const tempObject = useMemo(() => new THREE.Object3D(), []);
|
|
257
268
|
const tempColor = useMemo(() => new THREE.Color(), []);
|
|
269
|
+
// Track animated height multipliers for each building (for collapse animation)
|
|
270
|
+
const heightMultipliersRef = useRef(null);
|
|
271
|
+
const targetMultipliersRef = useRef(null);
|
|
272
|
+
// Check if highlight layers have any active items
|
|
273
|
+
const hasActiveHighlightLayers = useMemo(() => {
|
|
274
|
+
return highlightLayers.some(layer => layer.enabled && layer.items.length > 0);
|
|
275
|
+
}, [highlightLayers]);
|
|
276
|
+
// Initialize height multiplier arrays
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
if (buildings.length > 0) {
|
|
279
|
+
if (!heightMultipliersRef.current || heightMultipliersRef.current.length !== buildings.length) {
|
|
280
|
+
heightMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
281
|
+
targetMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}, [buildings.length]);
|
|
285
|
+
// Update target multipliers when focusDirectory or highlightLayers change
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
if (!targetMultipliersRef.current)
|
|
288
|
+
return;
|
|
289
|
+
buildings.forEach((building, index) => {
|
|
290
|
+
let shouldCollapse = false;
|
|
291
|
+
// Priority 1: focusDirectory - collapse buildings outside
|
|
292
|
+
if (focusDirectory) {
|
|
293
|
+
const isInFocus = isPathInDirectory(building.path, focusDirectory);
|
|
294
|
+
shouldCollapse = !isInFocus;
|
|
295
|
+
}
|
|
296
|
+
// Priority 2: highlightLayers with collapse isolation mode
|
|
297
|
+
else if (hasActiveHighlightLayers && isolationMode === 'collapse') {
|
|
298
|
+
const highlight = getHighlightForPath(building.path, highlightLayers);
|
|
299
|
+
shouldCollapse = highlight === null;
|
|
300
|
+
}
|
|
301
|
+
targetMultipliersRef.current[index] = shouldCollapse ? 0.05 : 1;
|
|
302
|
+
});
|
|
303
|
+
}, [focusDirectory, buildings, highlightLayers, isolationMode, hasActiveHighlightLayers]);
|
|
258
304
|
// Pre-compute building data
|
|
259
305
|
const buildingData = useMemo(() => {
|
|
260
306
|
return buildings.map((building, index) => {
|
|
261
307
|
const [width, , depth] = building.dimensions;
|
|
262
|
-
const highlight = getHighlightForPath(building.path, highlightLayers);
|
|
263
|
-
const isHighlighted = highlight !== null;
|
|
264
|
-
const shouldDim = hasActiveHighlights && !isHighlighted;
|
|
265
|
-
const shouldCollapse = shouldDim && isolationMode === 'collapse';
|
|
266
|
-
const shouldHide = shouldDim && isolationMode === 'hide';
|
|
267
308
|
const fullHeight = calculateBuildingHeight(building, heightScaling, linearScale);
|
|
268
|
-
const
|
|
269
|
-
const baseColor = getColorForFile(building);
|
|
270
|
-
const color = isHighlighted ? highlight.color : baseColor;
|
|
309
|
+
const color = getColorForFile(building);
|
|
271
310
|
const x = building.position.x - centerOffset.x;
|
|
272
311
|
const z = building.position.z - centerOffset.z;
|
|
273
312
|
const staggerIndex = staggerIndices[index] ?? index;
|
|
@@ -277,39 +316,30 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
277
316
|
index,
|
|
278
317
|
width,
|
|
279
318
|
depth,
|
|
280
|
-
|
|
319
|
+
fullHeight,
|
|
281
320
|
color,
|
|
282
321
|
x,
|
|
283
322
|
z,
|
|
284
|
-
shouldHide,
|
|
285
|
-
shouldDim,
|
|
286
323
|
staggerDelayMs,
|
|
287
|
-
isHighlighted,
|
|
288
324
|
};
|
|
289
325
|
});
|
|
290
|
-
}, [
|
|
291
|
-
buildings,
|
|
292
|
-
centerOffset,
|
|
293
|
-
highlightLayers,
|
|
294
|
-
hasActiveHighlights,
|
|
295
|
-
isolationMode,
|
|
296
|
-
heightScaling,
|
|
297
|
-
linearScale,
|
|
298
|
-
staggerIndices,
|
|
299
|
-
animationConfig.staggerDelay,
|
|
300
|
-
]);
|
|
301
|
-
const visibleBuildings = useMemo(() => buildingData.filter((b) => !b.shouldHide), [buildingData]);
|
|
326
|
+
}, [buildings, centerOffset, heightScaling, linearScale, staggerIndices, animationConfig.staggerDelay]);
|
|
302
327
|
const minHeight = 0.3;
|
|
303
328
|
const baseOffset = 0.2;
|
|
304
329
|
const tension = animationConfig.tension || 120;
|
|
305
330
|
const friction = animationConfig.friction || 14;
|
|
306
331
|
const springDuration = Math.sqrt(1 / (tension * 0.001)) * friction * 20;
|
|
332
|
+
// Initialize all buildings (only on first render or when building data changes)
|
|
333
|
+
// DO NOT include focusDirectory here - that would bypass the animation
|
|
334
|
+
const initializedRef = useRef(false);
|
|
307
335
|
useEffect(() => {
|
|
308
|
-
if (!meshRef.current)
|
|
336
|
+
if (!meshRef.current || buildingData.length === 0)
|
|
309
337
|
return;
|
|
310
|
-
|
|
311
|
-
const { width, depth, x, z, color,
|
|
312
|
-
|
|
338
|
+
buildingData.forEach((data, instanceIndex) => {
|
|
339
|
+
const { width, depth, x, z, color, fullHeight } = data;
|
|
340
|
+
// Use the current animated multiplier, or default to 1 on first render
|
|
341
|
+
const multiplier = heightMultipliersRef.current?.[instanceIndex] ?? 1;
|
|
342
|
+
const height = growProgress * fullHeight * multiplier + minHeight;
|
|
313
343
|
const yPosition = height / 2 + baseOffset;
|
|
314
344
|
tempObject.position.set(x, yPosition, z);
|
|
315
345
|
tempObject.scale.set(width, height, depth);
|
|
@@ -322,24 +352,29 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
322
352
|
if (meshRef.current.instanceColor) {
|
|
323
353
|
meshRef.current.instanceColor.needsUpdate = true;
|
|
324
354
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
tempObject,
|
|
329
|
-
tempColor,
|
|
330
|
-
minHeight,
|
|
331
|
-
baseOffset,
|
|
332
|
-
]);
|
|
355
|
+
initializedRef.current = true;
|
|
356
|
+
}, [buildingData, growProgress, tempObject, tempColor, minHeight, baseOffset]);
|
|
357
|
+
// Animate buildings each frame
|
|
333
358
|
useFrame(({ clock }) => {
|
|
334
|
-
if (!meshRef.current)
|
|
359
|
+
if (!meshRef.current || buildingData.length === 0)
|
|
360
|
+
return;
|
|
361
|
+
if (!heightMultipliersRef.current || !targetMultipliersRef.current)
|
|
335
362
|
return;
|
|
336
363
|
if (startTimeRef.current === null && growProgress > 0) {
|
|
337
364
|
startTimeRef.current = clock.elapsedTime * 1000;
|
|
338
365
|
}
|
|
339
366
|
const currentTime = clock.elapsedTime * 1000;
|
|
340
367
|
const animStartTime = startTimeRef.current ?? currentTime;
|
|
341
|
-
|
|
342
|
-
|
|
368
|
+
// Animation speed for collapse/expand (lerp factor per frame)
|
|
369
|
+
const collapseSpeed = 0.08;
|
|
370
|
+
buildingData.forEach((data, instanceIndex) => {
|
|
371
|
+
const { width, depth, fullHeight, x, z, staggerDelayMs } = data;
|
|
372
|
+
// Animate height multiplier towards target
|
|
373
|
+
const currentMultiplier = heightMultipliersRef.current[instanceIndex];
|
|
374
|
+
const targetMultiplier = targetMultipliersRef.current[instanceIndex];
|
|
375
|
+
const newMultiplier = currentMultiplier + (targetMultiplier - currentMultiplier) * collapseSpeed;
|
|
376
|
+
heightMultipliersRef.current[instanceIndex] = newMultiplier;
|
|
377
|
+
// Calculate grow animation progress
|
|
343
378
|
const elapsed = currentTime - animStartTime - staggerDelayMs;
|
|
344
379
|
let animProgress = growProgress;
|
|
345
380
|
if (growProgress > 0 && elapsed >= 0) {
|
|
@@ -350,7 +385,8 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
350
385
|
else if (growProgress > 0 && elapsed < 0) {
|
|
351
386
|
animProgress = 0;
|
|
352
387
|
}
|
|
353
|
-
|
|
388
|
+
// Apply both grow animation and collapse multiplier
|
|
389
|
+
const height = animProgress * fullHeight * newMultiplier + minHeight;
|
|
354
390
|
const yPosition = height / 2 + baseOffset;
|
|
355
391
|
const isHovered = hoveredIndex === data.index;
|
|
356
392
|
const scale = isHovered ? 1.05 : 1;
|
|
@@ -358,10 +394,15 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
358
394
|
tempObject.scale.set(width * scale, height, depth * scale);
|
|
359
395
|
tempObject.updateMatrix();
|
|
360
396
|
meshRef.current.setMatrixAt(instanceIndex, tempObject.matrix);
|
|
361
|
-
|
|
397
|
+
// Desaturate collapsed buildings
|
|
362
398
|
tempColor.set(data.color);
|
|
363
|
-
if (
|
|
364
|
-
|
|
399
|
+
if (newMultiplier < 0.5) {
|
|
400
|
+
// Lerp towards gray based on collapse amount
|
|
401
|
+
const grayAmount = 1 - newMultiplier * 2; // 0 at multiplier=0.5, 1 at multiplier=0
|
|
402
|
+
const gray = 0.3;
|
|
403
|
+
tempColor.r = tempColor.r * (1 - grayAmount) + gray * grayAmount;
|
|
404
|
+
tempColor.g = tempColor.g * (1 - grayAmount) + gray * grayAmount;
|
|
405
|
+
tempColor.b = tempColor.b * (1 - grayAmount) + gray * grayAmount;
|
|
365
406
|
}
|
|
366
407
|
if (isHovered) {
|
|
367
408
|
tempColor.multiplyScalar(1.2);
|
|
@@ -375,26 +416,32 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
375
416
|
});
|
|
376
417
|
const handlePointerMove = useCallback((e) => {
|
|
377
418
|
e.stopPropagation();
|
|
378
|
-
if (e.instanceId !== undefined &&
|
|
379
|
-
e.instanceId
|
|
380
|
-
const data = visibleBuildings[e.instanceId];
|
|
419
|
+
if (e.instanceId !== undefined && e.instanceId < buildingData.length) {
|
|
420
|
+
const data = buildingData[e.instanceId];
|
|
381
421
|
onHover?.(data.building);
|
|
382
422
|
}
|
|
383
|
-
}, [
|
|
423
|
+
}, [buildingData, onHover]);
|
|
384
424
|
const handlePointerOut = useCallback(() => {
|
|
385
425
|
onHover?.(null);
|
|
386
426
|
}, [onHover]);
|
|
387
427
|
const handleClick = useCallback((e) => {
|
|
388
428
|
e.stopPropagation();
|
|
389
|
-
if (e.instanceId !== undefined &&
|
|
390
|
-
e.instanceId
|
|
391
|
-
const data = visibleBuildings[e.instanceId];
|
|
429
|
+
if (e.instanceId !== undefined && e.instanceId < buildingData.length) {
|
|
430
|
+
const data = buildingData[e.instanceId];
|
|
392
431
|
onClick?.(data.building);
|
|
393
432
|
}
|
|
394
|
-
}, [
|
|
395
|
-
if (
|
|
433
|
+
}, [buildingData, onClick]);
|
|
434
|
+
if (buildingData.length === 0)
|
|
396
435
|
return null;
|
|
397
|
-
return (_jsxs("group", { children: [_jsxs("instancedMesh", { ref: meshRef, args: [undefined, undefined,
|
|
436
|
+
return (_jsxs("group", { children: [_jsxs("instancedMesh", { ref: meshRef, args: [undefined, undefined, buildingData.length], onPointerMove: handlePointerMove, onPointerOut: handlePointerOut, onClick: handleClick, frustumCulled: false, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { metalness: 0.1, roughness: 0.35 })] }), _jsx(BuildingEdges, { buildings: buildingData.map(d => ({
|
|
437
|
+
width: d.width,
|
|
438
|
+
depth: d.depth,
|
|
439
|
+
fullHeight: d.fullHeight,
|
|
440
|
+
x: d.x,
|
|
441
|
+
z: d.z,
|
|
442
|
+
staggerDelayMs: d.staggerDelayMs,
|
|
443
|
+
buildingIndex: d.index,
|
|
444
|
+
})), growProgress: growProgress, minHeight: minHeight, baseOffset: baseOffset, springDuration: springDuration, heightMultipliersRef: heightMultipliersRef })] }));
|
|
398
445
|
}
|
|
399
446
|
function AnimatedIcon({ x, z, targetHeight, iconSize, texture, opacity, growProgress, staggerDelayMs, springDuration, }) {
|
|
400
447
|
const spriteRef = useRef(null);
|
|
@@ -496,10 +543,55 @@ let cameraResetFn = null;
|
|
|
496
543
|
export function resetCamera() {
|
|
497
544
|
cameraResetFn?.();
|
|
498
545
|
}
|
|
499
|
-
function AnimatedCamera({ citySize, isFlat }) {
|
|
546
|
+
function AnimatedCamera({ citySize, isFlat, focusTarget }) {
|
|
500
547
|
const { camera } = useThree();
|
|
501
548
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
502
549
|
const controlsRef = useRef(null);
|
|
550
|
+
// Animated camera position and target
|
|
551
|
+
const targetPos = useMemo(() => {
|
|
552
|
+
if (focusTarget) {
|
|
553
|
+
// Position camera to look at focus target
|
|
554
|
+
const distance = Math.max(focusTarget.size * 2, 50);
|
|
555
|
+
const height = Math.max(focusTarget.size * 1.5, 40);
|
|
556
|
+
return {
|
|
557
|
+
x: focusTarget.x,
|
|
558
|
+
y: height,
|
|
559
|
+
z: focusTarget.z + distance,
|
|
560
|
+
targetX: focusTarget.x,
|
|
561
|
+
targetY: 0,
|
|
562
|
+
targetZ: focusTarget.z,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
// Default: overview of entire city
|
|
566
|
+
const targetHeight = isFlat ? citySize * 1.5 : citySize * 1.1;
|
|
567
|
+
const targetZ = isFlat ? 0 : citySize * 1.3;
|
|
568
|
+
return {
|
|
569
|
+
x: 0,
|
|
570
|
+
y: targetHeight,
|
|
571
|
+
z: targetZ,
|
|
572
|
+
targetX: 0,
|
|
573
|
+
targetY: 0,
|
|
574
|
+
targetZ: 0,
|
|
575
|
+
};
|
|
576
|
+
}, [focusTarget, isFlat, citySize]);
|
|
577
|
+
// Spring animation for camera movement
|
|
578
|
+
const { camX, camY, camZ, lookX, lookY, lookZ } = useSpring({
|
|
579
|
+
camX: targetPos.x,
|
|
580
|
+
camY: targetPos.y,
|
|
581
|
+
camZ: targetPos.z,
|
|
582
|
+
lookX: targetPos.targetX,
|
|
583
|
+
lookY: targetPos.targetY,
|
|
584
|
+
lookZ: targetPos.targetZ,
|
|
585
|
+
config: { tension: 60, friction: 20 },
|
|
586
|
+
});
|
|
587
|
+
// Update camera each frame based on spring values
|
|
588
|
+
useFrame(() => {
|
|
589
|
+
if (!controlsRef.current)
|
|
590
|
+
return;
|
|
591
|
+
camera.position.set(camX.get(), camY.get(), camZ.get());
|
|
592
|
+
controlsRef.current.target.set(lookX.get(), lookY.get(), lookZ.get());
|
|
593
|
+
controlsRef.current.update();
|
|
594
|
+
});
|
|
503
595
|
const resetToInitial = useCallback(() => {
|
|
504
596
|
const targetHeight = isFlat ? citySize * 1.5 : citySize * 1.1;
|
|
505
597
|
const targetZ = isFlat ? 0 : citySize * 1.3;
|
|
@@ -511,8 +603,10 @@ function AnimatedCamera({ citySize, isFlat }) {
|
|
|
511
603
|
}
|
|
512
604
|
}, [isFlat, citySize, camera]);
|
|
513
605
|
useEffect(() => {
|
|
514
|
-
|
|
515
|
-
|
|
606
|
+
if (!focusTarget) {
|
|
607
|
+
resetToInitial();
|
|
608
|
+
}
|
|
609
|
+
}, [resetToInitial, focusTarget]);
|
|
516
610
|
useEffect(() => {
|
|
517
611
|
cameraResetFn = resetToInitial;
|
|
518
612
|
return () => {
|
|
@@ -571,13 +665,132 @@ function ControlsOverlay({ isFlat, onToggle, onResetCamera, }) {
|
|
|
571
665
|
gap: 8,
|
|
572
666
|
}, children: [_jsx("button", { onClick: onResetCamera, style: buttonStyle, children: "Reset View" }), _jsx("button", { onClick: onToggle, style: buttonStyle, children: isFlat ? 'Grow to 3D' : 'Flatten to 2D' })] }));
|
|
573
667
|
}
|
|
574
|
-
function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding, growProgress, animationConfig, highlightLayers, isolationMode,
|
|
668
|
+
function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding, growProgress, animationConfig, highlightLayers, isolationMode, heightScaling, linearScale, focusDirectory, }) {
|
|
575
669
|
const centerOffset = useMemo(() => ({
|
|
576
670
|
x: (cityData.bounds.minX + cityData.bounds.maxX) / 2,
|
|
577
671
|
z: (cityData.bounds.minZ + cityData.bounds.maxZ) / 2,
|
|
578
672
|
}), [cityData.bounds]);
|
|
579
673
|
const citySize = Math.max(cityData.bounds.maxX - cityData.bounds.minX, cityData.bounds.maxZ - cityData.bounds.minZ);
|
|
580
674
|
const activeHighlights = useMemo(() => hasActiveHighlights(highlightLayers), [highlightLayers]);
|
|
675
|
+
// Helper to check if a path is inside a directory
|
|
676
|
+
const isPathInDirectory = useCallback((path, directory) => {
|
|
677
|
+
if (!directory)
|
|
678
|
+
return true;
|
|
679
|
+
return path === directory || path.startsWith(directory + '/');
|
|
680
|
+
}, []);
|
|
681
|
+
// Three-phase animation when switching directories:
|
|
682
|
+
// Phase 1: Camera zooms out to overview
|
|
683
|
+
// Phase 2: Buildings collapse/expand
|
|
684
|
+
// Phase 3: Camera zooms into new directory
|
|
685
|
+
//
|
|
686
|
+
// We track two separate states:
|
|
687
|
+
// - buildingFocusDirectory: controls which buildings are collapsed (passed to InstancedBuildings)
|
|
688
|
+
// - cameraFocusDirectory: controls camera position (used for focusTarget calculation)
|
|
689
|
+
const [buildingFocusDirectory, setBuildingFocusDirectory] = useState(null);
|
|
690
|
+
const [cameraFocusDirectory, setCameraFocusDirectory] = useState(null);
|
|
691
|
+
const prevFocusDirectoryRef = useRef(null);
|
|
692
|
+
const animationTimersRef = useRef([]);
|
|
693
|
+
useEffect(() => {
|
|
694
|
+
// Clear any pending timers
|
|
695
|
+
animationTimersRef.current.forEach(clearTimeout);
|
|
696
|
+
animationTimersRef.current = [];
|
|
697
|
+
const prevFocus = prevFocusDirectoryRef.current;
|
|
698
|
+
prevFocusDirectoryRef.current = focusDirectory;
|
|
699
|
+
// No change
|
|
700
|
+
if (focusDirectory === prevFocus)
|
|
701
|
+
return;
|
|
702
|
+
// Case 1: Going from overview to a directory (null -> dir)
|
|
703
|
+
if (prevFocus === null && focusDirectory !== null) {
|
|
704
|
+
// Phase 1: Collapse buildings immediately
|
|
705
|
+
setBuildingFocusDirectory(focusDirectory);
|
|
706
|
+
// Phase 2: After collapse settles, zoom camera in
|
|
707
|
+
const timer = setTimeout(() => {
|
|
708
|
+
setCameraFocusDirectory(focusDirectory);
|
|
709
|
+
}, 600);
|
|
710
|
+
animationTimersRef.current.push(timer);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
// Case 2: Going from a directory to overview (dir -> null)
|
|
714
|
+
if (prevFocus !== null && focusDirectory === null) {
|
|
715
|
+
// Phase 1: Zoom camera out first
|
|
716
|
+
setCameraFocusDirectory(null);
|
|
717
|
+
// Phase 2: After zoom-out settles, expand buildings
|
|
718
|
+
const timer = setTimeout(() => {
|
|
719
|
+
setBuildingFocusDirectory(null);
|
|
720
|
+
}, 500);
|
|
721
|
+
animationTimersRef.current.push(timer);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
// Case 3: Switching between directories (dirA -> dirB)
|
|
725
|
+
if (prevFocus !== null && focusDirectory !== null) {
|
|
726
|
+
// Phase 1: Zoom camera out
|
|
727
|
+
setCameraFocusDirectory(null);
|
|
728
|
+
// Phase 2: After zoom-out, collapse/expand buildings
|
|
729
|
+
const timer1 = setTimeout(() => {
|
|
730
|
+
setBuildingFocusDirectory(focusDirectory);
|
|
731
|
+
}, 500);
|
|
732
|
+
// Phase 3: After collapse settles, zoom camera into new directory
|
|
733
|
+
const timer2 = setTimeout(() => {
|
|
734
|
+
setCameraFocusDirectory(focusDirectory);
|
|
735
|
+
}, 1100); // 500ms zoom-out + 600ms collapse
|
|
736
|
+
animationTimersRef.current.push(timer1, timer2);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
}, [focusDirectory]);
|
|
740
|
+
// Cleanup timers on unmount
|
|
741
|
+
useEffect(() => {
|
|
742
|
+
return () => {
|
|
743
|
+
animationTimersRef.current.forEach(clearTimeout);
|
|
744
|
+
};
|
|
745
|
+
}, []);
|
|
746
|
+
// Calculate focus target from cameraFocusDirectory (for camera)
|
|
747
|
+
const focusTarget = useMemo(() => {
|
|
748
|
+
// Use camera focus directory for camera movement
|
|
749
|
+
if (cameraFocusDirectory) {
|
|
750
|
+
const focusedBuildings = cityData.buildings.filter((building) => isPathInDirectory(building.path, cameraFocusDirectory));
|
|
751
|
+
if (focusedBuildings.length === 0)
|
|
752
|
+
return null;
|
|
753
|
+
let minX = Infinity, maxX = -Infinity;
|
|
754
|
+
let minZ = Infinity, maxZ = -Infinity;
|
|
755
|
+
for (const building of focusedBuildings) {
|
|
756
|
+
const x = building.position.x - centerOffset.x;
|
|
757
|
+
const z = building.position.z - centerOffset.z;
|
|
758
|
+
const [width, , depth] = building.dimensions;
|
|
759
|
+
minX = Math.min(minX, x - width / 2);
|
|
760
|
+
maxX = Math.max(maxX, x + width / 2);
|
|
761
|
+
minZ = Math.min(minZ, z - depth / 2);
|
|
762
|
+
maxZ = Math.max(maxZ, z + depth / 2);
|
|
763
|
+
}
|
|
764
|
+
const centerX = (minX + maxX) / 2;
|
|
765
|
+
const centerZ = (minZ + maxZ) / 2;
|
|
766
|
+
const size = Math.max(maxX - minX, maxZ - minZ);
|
|
767
|
+
return { x: centerX, z: centerZ, size };
|
|
768
|
+
}
|
|
769
|
+
// Priority 2: highlight layers
|
|
770
|
+
if (!activeHighlights)
|
|
771
|
+
return null;
|
|
772
|
+
const highlightedBuildings = cityData.buildings.filter((building) => {
|
|
773
|
+
const highlight = getHighlightForPath(building.path, highlightLayers);
|
|
774
|
+
return highlight !== null;
|
|
775
|
+
});
|
|
776
|
+
if (highlightedBuildings.length === 0)
|
|
777
|
+
return null;
|
|
778
|
+
let minX = Infinity, maxX = -Infinity;
|
|
779
|
+
let minZ = Infinity, maxZ = -Infinity;
|
|
780
|
+
for (const building of highlightedBuildings) {
|
|
781
|
+
const x = building.position.x - centerOffset.x;
|
|
782
|
+
const z = building.position.z - centerOffset.z;
|
|
783
|
+
const [width, , depth] = building.dimensions;
|
|
784
|
+
minX = Math.min(minX, x - width / 2);
|
|
785
|
+
maxX = Math.max(maxX, x + width / 2);
|
|
786
|
+
minZ = Math.min(minZ, z - depth / 2);
|
|
787
|
+
maxZ = Math.max(maxZ, z + depth / 2);
|
|
788
|
+
}
|
|
789
|
+
const centerX = (minX + maxX) / 2;
|
|
790
|
+
const centerZ = (minZ + maxZ) / 2;
|
|
791
|
+
const size = Math.max(maxX - minX, maxZ - minZ);
|
|
792
|
+
return { x: centerX, z: centerZ, size };
|
|
793
|
+
}, [cameraFocusDirectory, activeHighlights, cityData.buildings, highlightLayers, centerOffset, isPathInDirectory]);
|
|
581
794
|
const staggerIndices = useMemo(() => {
|
|
582
795
|
const centerX = (cityData.bounds.minX + cityData.bounds.maxX) / 2;
|
|
583
796
|
const centerZ = (cityData.bounds.minZ + cityData.bounds.maxZ) / 2;
|
|
@@ -602,7 +815,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
602
815
|
const tension = animationConfig.tension || 120;
|
|
603
816
|
const friction = animationConfig.friction || 14;
|
|
604
817
|
const springDuration = Math.sqrt(1 / (tension * 0.001)) * friction * 20;
|
|
605
|
-
return (_jsxs(_Fragment, { children: [_jsx(AnimatedCamera, { citySize: citySize, isFlat: growProgress === 0 }), _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,
|
|
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((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 })] }));
|
|
606
819
|
}
|
|
607
820
|
/**
|
|
608
821
|
* FileCity3D - 3D visualization of codebase structure
|
|
@@ -610,7 +823,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
610
823
|
* Renders CityData as an interactive 3D city where buildings represent files
|
|
611
824
|
* and their height corresponds to line count or file size.
|
|
612
825
|
*/
|
|
613
|
-
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, }) {
|
|
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, }) {
|
|
614
827
|
const { theme } = useTheme();
|
|
615
828
|
const [hoveredBuilding, setHoveredBuilding] = useState(null);
|
|
616
829
|
const [internalIsGrown, setInternalIsGrown] = useState(false);
|
|
@@ -681,6 +894,6 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
681
894
|
left: 0,
|
|
682
895
|
width: '100%',
|
|
683
896
|
height: '100%',
|
|
684
|
-
}, children: _jsx(CityScene, { cityData: cityData, onBuildingHover: setHoveredBuilding, onBuildingClick: onBuildingClick, hoveredBuilding: hoveredBuilding, growProgress: growProgress, animationConfig: animationConfig, highlightLayers: highlightLayers, isolationMode: isolationMode,
|
|
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 }))] }));
|
|
685
898
|
}
|
|
686
899
|
export default FileCity3D;
|