@principal-ai/file-city-react 0.5.3 → 0.5.5

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.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * FileCity3D - 3D visualization of a codebase using React Three Fiber
3
+ *
4
+ * Renders CityData from file-city-builder as actual 3D buildings with
5
+ * camera controls, lighting, and interactivity.
6
+ *
7
+ * Supports animated transition from 2D (flat) to 3D (grown buildings).
8
+ */
9
+ import React from 'react';
10
+ import type { CityData, CityBuilding, CityDistrict } from '@principal-ai/file-city-builder';
11
+ import type { ThreeElements } from '@react-three/fiber';
12
+ declare module 'react' {
13
+ namespace JSX {
14
+ interface IntrinsicElements extends ThreeElements {
15
+ }
16
+ }
17
+ }
18
+ export type { CityData, CityBuilding, CityDistrict };
19
+ export interface HighlightLayer {
20
+ /** Unique identifier */
21
+ id: string;
22
+ /** Display name */
23
+ name: string;
24
+ /** Whether layer is active */
25
+ enabled: boolean;
26
+ /** Highlight color (hex) */
27
+ color: string;
28
+ /** Items to highlight */
29
+ items: HighlightItem[];
30
+ /** Opacity for highlighted items (0-1) */
31
+ opacity?: number;
32
+ }
33
+ export interface HighlightItem {
34
+ /** File or directory path */
35
+ path: string;
36
+ /** Type of item */
37
+ type: 'file' | 'directory';
38
+ }
39
+ /** What to do with non-highlighted buildings */
40
+ export type IsolationMode = 'none' | 'transparent' | 'collapse' | 'hide';
41
+ export interface AnimationConfig {
42
+ /** Start with buildings flat (2D view) */
43
+ startFlat?: boolean;
44
+ /** Auto-start the grow animation after this delay (ms). Set to null to disable. */
45
+ autoStartDelay?: number | null;
46
+ /** Duration of the grow animation in ms */
47
+ growDuration?: number;
48
+ /** Stagger delay between buildings in ms */
49
+ staggerDelay?: number;
50
+ /** Spring tension (higher = faster/snappier) */
51
+ tension?: number;
52
+ /** Spring friction (higher = less bouncy) */
53
+ friction?: number;
54
+ }
55
+ /** Height scaling mode for buildings */
56
+ export type HeightScaling = 'logarithmic' | 'linear';
57
+ export declare function resetCamera(): void;
58
+ export interface FileCity3DProps {
59
+ /** City data from file-city-builder */
60
+ cityData: CityData;
61
+ /** Width of the container */
62
+ width?: number | string;
63
+ /** Height of the container */
64
+ height?: number | string;
65
+ /** Callback when a building is clicked */
66
+ onBuildingClick?: (building: CityBuilding) => void;
67
+ /** CSS class name */
68
+ className?: string;
69
+ /** Inline styles */
70
+ style?: React.CSSProperties;
71
+ /** Animation configuration */
72
+ animation?: AnimationConfig;
73
+ /** External control: set to true to grow buildings, false to flatten */
74
+ isGrown?: boolean;
75
+ /** Callback when grow state changes */
76
+ onGrowChange?: (isGrown: boolean) => void;
77
+ /** Show control buttons */
78
+ showControls?: boolean;
79
+ /** Highlight layers for focusing on specific files/directories */
80
+ highlightLayers?: HighlightLayer[];
81
+ /** How to handle non-highlighted buildings when highlights are active */
82
+ isolationMode?: IsolationMode;
83
+ /** Opacity for dimmed buildings in transparent mode (0-1) */
84
+ dimOpacity?: number;
85
+ /** Whether data is currently loading */
86
+ isLoading?: boolean;
87
+ /** Message to display while loading */
88
+ loadingMessage?: string;
89
+ /** Message to display when there's no data */
90
+ emptyMessage?: string;
91
+ /** Height scaling mode: 'logarithmic' (default) or 'linear' */
92
+ heightScaling?: HeightScaling;
93
+ /** Scale factor for linear mode (height per line, default 0.05) */
94
+ linearScale?: number;
95
+ }
96
+ /**
97
+ * FileCity3D - 3D visualization of codebase structure
98
+ *
99
+ * Renders CityData as an interactive 3D city where buildings represent files
100
+ * and their height corresponds to line count or file size.
101
+ */
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): React.JSX.Element;
103
+ export default FileCity3D;
104
+ //# sourceMappingURL=FileCity3D.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,547 @@
1
+ "use strict";
2
+ /**
3
+ * FileCity3D - 3D visualization of a codebase using React Three Fiber
4
+ *
5
+ * Renders CityData from file-city-builder as actual 3D buildings with
6
+ * camera controls, lighting, and interactivity.
7
+ *
8
+ * Supports animated transition from 2D (flat) to 3D (grown buildings).
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.resetCamera = resetCamera;
45
+ exports.FileCity3D = FileCity3D;
46
+ const react_1 = __importStar(require("react"));
47
+ const fiber_1 = require("@react-three/fiber");
48
+ const industry_theme_1 = require("@principal-ade/industry-theme");
49
+ const three_1 = require("@react-spring/three");
50
+ const drei_1 = require("@react-three/drei");
51
+ const file_city_builder_1 = require("@principal-ai/file-city-builder");
52
+ const THREE = __importStar(require("three"));
53
+ const DEFAULT_ANIMATION = {
54
+ startFlat: false,
55
+ autoStartDelay: 500,
56
+ growDuration: 1500,
57
+ staggerDelay: 15,
58
+ tension: 120,
59
+ friction: 14,
60
+ };
61
+ // Code file extensions - height based on line count
62
+ const CODE_EXTENSIONS = new Set([
63
+ 'ts',
64
+ 'tsx',
65
+ 'js',
66
+ 'jsx',
67
+ 'mjs',
68
+ 'cjs',
69
+ 'py',
70
+ 'pyw',
71
+ 'rs',
72
+ 'go',
73
+ 'java',
74
+ 'kt',
75
+ 'scala',
76
+ 'c',
77
+ 'cpp',
78
+ 'cc',
79
+ 'cxx',
80
+ 'h',
81
+ 'hpp',
82
+ 'cs',
83
+ 'rb',
84
+ 'php',
85
+ 'swift',
86
+ 'vue',
87
+ 'svelte',
88
+ 'lua',
89
+ 'sh',
90
+ 'bash',
91
+ 'zsh',
92
+ 'sql',
93
+ 'r',
94
+ 'dart',
95
+ 'elm',
96
+ 'ex',
97
+ 'exs',
98
+ 'clj',
99
+ 'cljs',
100
+ 'hs',
101
+ 'ml',
102
+ 'mli',
103
+ ]);
104
+ function isCodeFile(extension) {
105
+ return CODE_EXTENSIONS.has(extension.toLowerCase());
106
+ }
107
+ /**
108
+ * Calculate building height based on file metrics.
109
+ * - logarithmic: Compresses large values (default, good for mixed codebases)
110
+ * - linear: Direct scaling (1 line = linearScale units of height)
111
+ */
112
+ function calculateBuildingHeight(building, scaling = 'logarithmic', linearScale = 0.05) {
113
+ const minHeight = 2;
114
+ // Use lineCount if available (any text file), otherwise fall back to size
115
+ if (building.lineCount !== undefined) {
116
+ const lines = Math.max(building.lineCount, 1);
117
+ if (scaling === 'linear') {
118
+ return minHeight + lines * linearScale;
119
+ }
120
+ // Logarithmic: log10(10) = 1, log10(100) = 2, log10(1000) = 3
121
+ return minHeight + Math.log10(lines) * 12;
122
+ }
123
+ else if (building.size !== undefined) {
124
+ const bytes = Math.max(building.size, 1);
125
+ if (scaling === 'linear') {
126
+ return minHeight + (bytes / 1024) * linearScale;
127
+ }
128
+ // Logarithmic scale based on size
129
+ return minHeight + (Math.log10(bytes) - 2) * 12;
130
+ }
131
+ // Fallback to dimension height if no metrics available
132
+ return building.dimensions[1];
133
+ }
134
+ // Get full file config from centralized file-city-builder lookup
135
+ function getConfigForFile(building) {
136
+ if (building.color) {
137
+ return {
138
+ color: building.color,
139
+ renderStrategy: 'fill',
140
+ opacity: 1,
141
+ matchedPattern: 'preset',
142
+ matchType: 'filename',
143
+ };
144
+ }
145
+ return (0, file_city_builder_1.getFileConfig)(building.path);
146
+ }
147
+ function getColorForFile(building) {
148
+ return getConfigForFile(building).color;
149
+ }
150
+ /**
151
+ * Check if a path is highlighted by any enabled layer.
152
+ */
153
+ function getHighlightForPath(path, layers) {
154
+ for (const layer of layers) {
155
+ if (!layer.enabled)
156
+ continue;
157
+ for (const item of layer.items) {
158
+ if (item.type === 'file' && item.path === path) {
159
+ return { color: layer.color, opacity: layer.opacity ?? 1 };
160
+ }
161
+ if (item.type === 'directory' &&
162
+ (path === item.path || path.startsWith(item.path + '/'))) {
163
+ return { color: layer.color, opacity: layer.opacity ?? 1 };
164
+ }
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+ function hasActiveHighlights(layers) {
170
+ return layers.some((layer) => layer.enabled && layer.items.length > 0);
171
+ }
172
+ // Animated RoundedBox wrapper
173
+ const AnimatedRoundedBox = (0, three_1.animated)(drei_1.RoundedBox);
174
+ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hoveredIndex, growProgress, animationConfig, highlightLayers, isolationMode, hasActiveHighlights, dimOpacity, heightScaling, linearScale, staggerIndices, }) {
175
+ const meshRef = (0, react_1.useRef)(null);
176
+ const startTimeRef = (0, react_1.useRef)(null);
177
+ const tempObject = (0, react_1.useMemo)(() => new THREE.Object3D(), []);
178
+ const tempColor = (0, react_1.useMemo)(() => new THREE.Color(), []);
179
+ // Pre-compute building data
180
+ const buildingData = (0, react_1.useMemo)(() => {
181
+ return buildings.map((building, index) => {
182
+ const [width, , depth] = building.dimensions;
183
+ const highlight = getHighlightForPath(building.path, highlightLayers);
184
+ const isHighlighted = highlight !== null;
185
+ const shouldDim = hasActiveHighlights && !isHighlighted;
186
+ const shouldCollapse = shouldDim && isolationMode === 'collapse';
187
+ const shouldHide = shouldDim && isolationMode === 'hide';
188
+ const fullHeight = calculateBuildingHeight(building, heightScaling, linearScale);
189
+ const targetHeight = shouldCollapse ? 0.5 : fullHeight;
190
+ const baseColor = getColorForFile(building);
191
+ const color = isHighlighted ? highlight.color : baseColor;
192
+ const x = building.position.x - centerOffset.x;
193
+ const z = building.position.z - centerOffset.z;
194
+ const staggerIndex = staggerIndices[index] ?? index;
195
+ const staggerDelayMs = (animationConfig.staggerDelay || 15) * staggerIndex;
196
+ return {
197
+ building,
198
+ index,
199
+ width,
200
+ depth,
201
+ targetHeight,
202
+ color,
203
+ x,
204
+ z,
205
+ shouldHide,
206
+ shouldDim,
207
+ staggerDelayMs,
208
+ isHighlighted,
209
+ };
210
+ });
211
+ }, [
212
+ buildings,
213
+ centerOffset,
214
+ highlightLayers,
215
+ hasActiveHighlights,
216
+ isolationMode,
217
+ heightScaling,
218
+ linearScale,
219
+ staggerIndices,
220
+ animationConfig.staggerDelay,
221
+ ]);
222
+ const visibleBuildings = (0, react_1.useMemo)(() => buildingData.filter((b) => !b.shouldHide), [buildingData]);
223
+ const minHeight = 0.3;
224
+ const baseOffset = 0.2;
225
+ const tension = animationConfig.tension || 120;
226
+ const friction = animationConfig.friction || 14;
227
+ const springDuration = Math.sqrt(1 / (tension * 0.001)) * friction * 20;
228
+ (0, react_1.useEffect)(() => {
229
+ if (!meshRef.current)
230
+ return;
231
+ visibleBuildings.forEach((data, instanceIndex) => {
232
+ const { width, depth, x, z, color, targetHeight } = data;
233
+ const height = growProgress * targetHeight + minHeight;
234
+ const yPosition = height / 2 + baseOffset;
235
+ tempObject.position.set(x, yPosition, z);
236
+ tempObject.scale.set(width, height, depth);
237
+ tempObject.updateMatrix();
238
+ meshRef.current.setMatrixAt(instanceIndex, tempObject.matrix);
239
+ tempColor.set(color);
240
+ meshRef.current.setColorAt(instanceIndex, tempColor);
241
+ });
242
+ meshRef.current.instanceMatrix.needsUpdate = true;
243
+ if (meshRef.current.instanceColor) {
244
+ meshRef.current.instanceColor.needsUpdate = true;
245
+ }
246
+ }, [
247
+ visibleBuildings,
248
+ growProgress,
249
+ tempObject,
250
+ tempColor,
251
+ minHeight,
252
+ baseOffset,
253
+ ]);
254
+ (0, fiber_1.useFrame)(({ clock }) => {
255
+ if (!meshRef.current)
256
+ return;
257
+ if (startTimeRef.current === null && growProgress > 0) {
258
+ startTimeRef.current = clock.elapsedTime * 1000;
259
+ }
260
+ const currentTime = clock.elapsedTime * 1000;
261
+ const animStartTime = startTimeRef.current ?? currentTime;
262
+ visibleBuildings.forEach((data, instanceIndex) => {
263
+ const { width, depth, targetHeight, x, z, staggerDelayMs, shouldDim } = data;
264
+ const elapsed = currentTime - animStartTime - staggerDelayMs;
265
+ let animProgress = growProgress;
266
+ if (growProgress > 0 && elapsed >= 0) {
267
+ const t = Math.min(elapsed / springDuration, 1);
268
+ const eased = 1 - Math.pow(1 - t, 3);
269
+ animProgress = eased * growProgress;
270
+ }
271
+ else if (growProgress > 0 && elapsed < 0) {
272
+ animProgress = 0;
273
+ }
274
+ const height = animProgress * targetHeight + minHeight;
275
+ const yPosition = height / 2 + baseOffset;
276
+ const isHovered = hoveredIndex === data.index;
277
+ const scale = isHovered ? 1.05 : 1;
278
+ tempObject.position.set(x, yPosition, z);
279
+ tempObject.scale.set(width * scale, height, depth * scale);
280
+ tempObject.updateMatrix();
281
+ meshRef.current.setMatrixAt(instanceIndex, tempObject.matrix);
282
+ const opacity = shouldDim && isolationMode === 'transparent' ? dimOpacity : 1;
283
+ tempColor.set(data.color);
284
+ if (opacity < 1) {
285
+ tempColor.multiplyScalar(opacity + 0.3);
286
+ }
287
+ if (isHovered) {
288
+ tempColor.multiplyScalar(1.2);
289
+ }
290
+ meshRef.current.setColorAt(instanceIndex, tempColor);
291
+ });
292
+ meshRef.current.instanceMatrix.needsUpdate = true;
293
+ if (meshRef.current.instanceColor) {
294
+ meshRef.current.instanceColor.needsUpdate = true;
295
+ }
296
+ });
297
+ const handlePointerMove = (0, react_1.useCallback)((e) => {
298
+ e.stopPropagation();
299
+ if (e.instanceId !== undefined &&
300
+ e.instanceId < visibleBuildings.length) {
301
+ const data = visibleBuildings[e.instanceId];
302
+ onHover?.(data.building);
303
+ }
304
+ }, [visibleBuildings, onHover]);
305
+ const handlePointerOut = (0, react_1.useCallback)(() => {
306
+ onHover?.(null);
307
+ }, [onHover]);
308
+ const handleClick = (0, react_1.useCallback)((e) => {
309
+ e.stopPropagation();
310
+ if (e.instanceId !== undefined &&
311
+ e.instanceId < visibleBuildings.length) {
312
+ const data = visibleBuildings[e.instanceId];
313
+ onClick?.(data.building);
314
+ }
315
+ }, [visibleBuildings, onClick]);
316
+ if (visibleBuildings.length === 0)
317
+ return null;
318
+ return (react_1.default.createElement("instancedMesh", { ref: meshRef, args: [undefined, undefined, visibleBuildings.length], onPointerMove: handlePointerMove, onPointerOut: handlePointerOut, onClick: handleClick, frustumCulled: false },
319
+ react_1.default.createElement("boxGeometry", { args: [1, 1, 1] }),
320
+ react_1.default.createElement("meshStandardMaterial", { metalness: 0.1, roughness: 0.35 })));
321
+ }
322
+ function DistrictFloor({ district, centerOffset, opacity, }) {
323
+ const { worldBounds } = district;
324
+ const width = worldBounds.maxX - worldBounds.minX;
325
+ const depth = worldBounds.maxZ - worldBounds.minZ;
326
+ const centerX = (worldBounds.minX + worldBounds.maxX) / 2 - centerOffset.x;
327
+ const centerZ = (worldBounds.minZ + worldBounds.maxZ) / 2 - centerOffset.z;
328
+ const dirName = district.path.split('/').pop() || district.path;
329
+ const pathDepth = district.path.split('/').length;
330
+ const floorY = -5 - pathDepth * 0.1;
331
+ return (react_1.default.createElement("group", { position: [centerX, 0, centerZ] },
332
+ react_1.default.createElement("lineSegments", { rotation: [-Math.PI / 2, 0, 0], position: [0, floorY, 0], renderOrder: -1 },
333
+ react_1.default.createElement("edgesGeometry", { args: [new THREE.PlaneGeometry(width, depth)], attach: "geometry" }),
334
+ react_1.default.createElement("lineBasicMaterial", { color: "#475569", depthWrite: false })),
335
+ district.label && (react_1.default.createElement(drei_1.Text, { position: [0, 1.5, depth / 2 + 2], rotation: [-Math.PI / 6, 0, 0], fontSize: Math.min(3, width / 6), color: "#cbd5e1", anchorX: "center", anchorY: "middle", outlineWidth: 0.1, outlineColor: "#0f172a" }, dirName))));
336
+ }
337
+ let cameraResetFn = null;
338
+ function resetCamera() {
339
+ cameraResetFn?.();
340
+ }
341
+ function AnimatedCamera({ citySize, isFlat }) {
342
+ const { camera } = (0, fiber_1.useThree)();
343
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
344
+ const controlsRef = (0, react_1.useRef)(null);
345
+ const resetToInitial = (0, react_1.useCallback)(() => {
346
+ const targetHeight = isFlat ? citySize * 1.5 : citySize * 1.1;
347
+ const targetZ = isFlat ? 0 : citySize * 1.3;
348
+ camera.position.set(0, targetHeight, targetZ);
349
+ camera.lookAt(0, 0, 0);
350
+ if (controlsRef.current) {
351
+ controlsRef.current.target.set(0, 0, 0);
352
+ controlsRef.current.update();
353
+ }
354
+ }, [isFlat, citySize, camera]);
355
+ (0, react_1.useEffect)(() => {
356
+ resetToInitial();
357
+ }, [resetToInitial]);
358
+ (0, react_1.useEffect)(() => {
359
+ cameraResetFn = resetToInitial;
360
+ return () => {
361
+ cameraResetFn = null;
362
+ };
363
+ }, [resetToInitial]);
364
+ return (react_1.default.createElement(react_1.default.Fragment, null,
365
+ react_1.default.createElement(drei_1.PerspectiveCamera, { makeDefault: true, fov: 50, near: 1, far: citySize * 10 }),
366
+ react_1.default.createElement(drei_1.OrbitControls, { ref: controlsRef, enableDamping: true, dampingFactor: 0.05, minDistance: 10, maxDistance: citySize * 3, maxPolarAngle: Math.PI / 2.1 })));
367
+ }
368
+ function InfoPanel({ building }) {
369
+ if (!building)
370
+ return null;
371
+ const fileName = building.path.split('/').pop();
372
+ const dirPath = building.path.split('/').slice(0, -1).join('/');
373
+ const rawExt = building.fileExtension || building.path.split('.').pop() || '';
374
+ const ext = rawExt.replace(/^\./, '');
375
+ const isCode = isCodeFile(ext);
376
+ return (react_1.default.createElement("div", { style: {
377
+ position: 'absolute',
378
+ bottom: 16,
379
+ left: 16,
380
+ background: 'rgba(15, 23, 42, 0.9)',
381
+ border: '1px solid #334155',
382
+ borderRadius: 8,
383
+ padding: '12px 16px',
384
+ color: '#e2e8f0',
385
+ fontSize: 14,
386
+ fontFamily: 'monospace',
387
+ maxWidth: 400,
388
+ pointerEvents: 'none',
389
+ } },
390
+ react_1.default.createElement("div", { style: { fontWeight: 600, marginBottom: 4 } }, fileName),
391
+ react_1.default.createElement("div", { style: { color: '#94a3b8', fontSize: 12 } }, dirPath),
392
+ react_1.default.createElement("div", { style: {
393
+ color: '#64748b',
394
+ fontSize: 11,
395
+ marginTop: 4,
396
+ display: 'flex',
397
+ gap: 12,
398
+ } },
399
+ building.lineCount !== undefined && (react_1.default.createElement("span", null,
400
+ building.lineCount.toLocaleString(),
401
+ " lines")),
402
+ building.size !== undefined && (react_1.default.createElement("span", null,
403
+ (building.size / 1024).toFixed(1),
404
+ " KB")))));
405
+ }
406
+ function ControlsOverlay({ isFlat, onToggle, onResetCamera, }) {
407
+ const buttonStyle = {
408
+ background: 'rgba(15, 23, 42, 0.9)',
409
+ border: '1px solid #334155',
410
+ borderRadius: 6,
411
+ padding: '8px 16px',
412
+ color: '#e2e8f0',
413
+ fontSize: 13,
414
+ cursor: 'pointer',
415
+ display: 'flex',
416
+ alignItems: 'center',
417
+ gap: 6,
418
+ };
419
+ return (react_1.default.createElement("div", { style: {
420
+ position: 'absolute',
421
+ top: 16,
422
+ right: 16,
423
+ display: 'flex',
424
+ gap: 8,
425
+ } },
426
+ react_1.default.createElement("button", { onClick: onResetCamera, style: buttonStyle }, "Reset View"),
427
+ react_1.default.createElement("button", { onClick: onToggle, style: buttonStyle }, isFlat ? 'Grow to 3D' : 'Flatten to 2D')));
428
+ }
429
+ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding, growProgress, animationConfig, highlightLayers, isolationMode, dimOpacity, heightScaling, linearScale, }) {
430
+ const centerOffset = (0, react_1.useMemo)(() => ({
431
+ x: (cityData.bounds.minX + cityData.bounds.maxX) / 2,
432
+ z: (cityData.bounds.minZ + cityData.bounds.maxZ) / 2,
433
+ }), [cityData.bounds]);
434
+ const citySize = Math.max(cityData.bounds.maxX - cityData.bounds.minX, cityData.bounds.maxZ - cityData.bounds.minZ);
435
+ const activeHighlights = (0, react_1.useMemo)(() => hasActiveHighlights(highlightLayers), [highlightLayers]);
436
+ const staggerIndices = (0, react_1.useMemo)(() => {
437
+ const centerX = (cityData.bounds.minX + cityData.bounds.maxX) / 2;
438
+ const centerZ = (cityData.bounds.minZ + cityData.bounds.maxZ) / 2;
439
+ const withDistance = cityData.buildings.map((b, originalIndex) => ({
440
+ originalIndex,
441
+ distance: Math.sqrt(Math.pow(b.position.x - centerX, 2) +
442
+ Math.pow(b.position.z - centerZ, 2)),
443
+ }));
444
+ withDistance.sort((a, b) => a.distance - b.distance);
445
+ const indices = new Array(cityData.buildings.length);
446
+ withDistance.forEach((item, staggerOrder) => {
447
+ indices[item.originalIndex] = staggerOrder;
448
+ });
449
+ return indices;
450
+ }, [cityData.buildings, cityData.bounds]);
451
+ const hoveredIndex = (0, react_1.useMemo)(() => {
452
+ if (!hoveredBuilding)
453
+ return null;
454
+ return cityData.buildings.findIndex((b) => b.path === hoveredBuilding.path);
455
+ }, [hoveredBuilding, cityData.buildings]);
456
+ return (react_1.default.createElement(react_1.default.Fragment, null,
457
+ react_1.default.createElement(AnimatedCamera, { citySize: citySize, isFlat: growProgress === 0 }),
458
+ react_1.default.createElement("ambientLight", { intensity: 0.4 }),
459
+ react_1.default.createElement("directionalLight", { position: [citySize, citySize, citySize * 0.5], intensity: 1, castShadow: true, "shadow-mapSize": [2048, 2048] }),
460
+ react_1.default.createElement("directionalLight", { position: [-citySize * 0.5, citySize * 0.5, -citySize * 0.5], intensity: 0.3 }),
461
+ cityData.districts.map((district) => (react_1.default.createElement(DistrictFloor, { key: district.path, district: district, centerOffset: centerOffset, opacity: 1 }))),
462
+ react_1.default.createElement(InstancedBuildings, { buildings: cityData.buildings, centerOffset: centerOffset, onHover: onBuildingHover, onClick: onBuildingClick, hoveredIndex: hoveredIndex, growProgress: growProgress, animationConfig: animationConfig, highlightLayers: highlightLayers, isolationMode: isolationMode, hasActiveHighlights: activeHighlights, dimOpacity: dimOpacity, heightScaling: heightScaling, linearScale: linearScale, staggerIndices: staggerIndices })));
463
+ }
464
+ /**
465
+ * FileCity3D - 3D visualization of codebase structure
466
+ *
467
+ * Renders CityData as an interactive 3D city where buildings represent files
468
+ * and their height corresponds to line count or file size.
469
+ */
470
+ 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, }) {
471
+ const { theme } = (0, industry_theme_1.useTheme)();
472
+ const [hoveredBuilding, setHoveredBuilding] = (0, react_1.useState)(null);
473
+ const [internalIsGrown, setInternalIsGrown] = (0, react_1.useState)(false);
474
+ const animationConfig = (0, react_1.useMemo)(() => ({ ...DEFAULT_ANIMATION, ...animation }), [animation]);
475
+ const isGrown = externalIsGrown !== undefined ? externalIsGrown : internalIsGrown;
476
+ const setIsGrown = (value) => {
477
+ setInternalIsGrown(value);
478
+ onGrowChange?.(value);
479
+ };
480
+ (0, react_1.useEffect)(() => {
481
+ if (animationConfig.startFlat && animationConfig.autoStartDelay !== null) {
482
+ const timer = setTimeout(() => {
483
+ setIsGrown(true);
484
+ }, animationConfig.autoStartDelay);
485
+ return () => clearTimeout(timer);
486
+ }
487
+ else if (!animationConfig.startFlat) {
488
+ setIsGrown(true);
489
+ }
490
+ // eslint-disable-next-line react-hooks/exhaustive-deps
491
+ }, [animationConfig.startFlat, animationConfig.autoStartDelay]);
492
+ const growProgress = isGrown ? 1 : 0;
493
+ const handleToggle = () => {
494
+ setIsGrown(!isGrown);
495
+ };
496
+ if (isLoading) {
497
+ return (react_1.default.createElement("div", { className: className, style: {
498
+ width,
499
+ height,
500
+ position: 'relative',
501
+ background: theme.colors.background,
502
+ overflow: 'hidden',
503
+ display: 'flex',
504
+ alignItems: 'center',
505
+ justifyContent: 'center',
506
+ color: theme.colors.textSecondary,
507
+ fontFamily: 'system-ui, sans-serif',
508
+ fontSize: 14,
509
+ ...style,
510
+ } }, loadingMessage));
511
+ }
512
+ if (!cityData || cityData.buildings.length === 0) {
513
+ return (react_1.default.createElement("div", { className: className, style: {
514
+ width,
515
+ height,
516
+ position: 'relative',
517
+ background: theme.colors.background,
518
+ overflow: 'hidden',
519
+ display: 'flex',
520
+ alignItems: 'center',
521
+ justifyContent: 'center',
522
+ color: theme.colors.textSecondary,
523
+ fontFamily: 'system-ui, sans-serif',
524
+ fontSize: 14,
525
+ ...style,
526
+ } }, emptyMessage));
527
+ }
528
+ return (react_1.default.createElement("div", { className: className, style: {
529
+ width,
530
+ height,
531
+ position: 'relative',
532
+ background: theme.colors.background,
533
+ overflow: 'hidden',
534
+ ...style,
535
+ } },
536
+ react_1.default.createElement(fiber_1.Canvas, { shadows: true, style: {
537
+ position: 'absolute',
538
+ top: 0,
539
+ left: 0,
540
+ width: '100%',
541
+ height: '100%',
542
+ } },
543
+ react_1.default.createElement(CityScene, { cityData: cityData, onBuildingHover: setHoveredBuilding, onBuildingClick: onBuildingClick, hoveredBuilding: hoveredBuilding, growProgress: growProgress, animationConfig: animationConfig, highlightLayers: highlightLayers, isolationMode: isolationMode, dimOpacity: dimOpacity, heightScaling: heightScaling, linearScale: linearScale })),
544
+ react_1.default.createElement(InfoPanel, { building: hoveredBuilding }),
545
+ showControls && (react_1.default.createElement(ControlsOverlay, { isFlat: !isGrown, onToggle: handleToggle, onResetCamera: resetCamera }))));
546
+ }
547
+ exports.default = FileCity3D;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * FileCity3D - 3D visualization component
3
+ */
4
+ export { FileCity3D, resetCamera } from './FileCity3D';
5
+ export type { FileCity3DProps, AnimationConfig, HighlightLayer, HighlightItem, IsolationMode, HeightScaling, CityData, CityBuilding, CityDistrict, } from './FileCity3D';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EACV,eAAe,EACf,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,EACb,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,YAAY,GACb,MAAM,cAAc,CAAC"}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * FileCity3D - 3D visualization component
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resetCamera = exports.FileCity3D = void 0;
7
+ var FileCity3D_1 = require("./FileCity3D");
8
+ Object.defineProperty(exports, "FileCity3D", { enumerable: true, get: function () { return FileCity3D_1.FileCity3D; } });
9
+ Object.defineProperty(exports, "resetCamera", { enumerable: true, get: function () { return FileCity3D_1.resetCamera; } });