@principal-ai/file-city-react 0.5.2 → 0.5.4

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,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; } });
package/dist/index.d.ts CHANGED
@@ -13,4 +13,6 @@ export type { UseCodeCityDataOptions, UseCodeCityDataReturn } from './hooks/useC
13
13
  export type { FileTree } from '@principal-ai/file-city-builder';
14
14
  export { CityViewWithReactFlow, type CityViewWithReactFlowProps, } from './components/CityViewWithReactFlow';
15
15
  export { ThemeProvider, useTheme } from '@principal-ade/industry-theme';
16
+ export { FileCity3D, resetCamera } from './components/FileCity3D';
17
+ export type { FileCity3DProps, AnimationConfig, HighlightLayer as FileCity3DHighlightLayer, HighlightItem as FileCity3DHighlightItem, IsolationMode, HeightScaling, } from './components/FileCity3D';
16
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,KAAK,mCAAmC,GACzC,MAAM,6CAA6C,CAAC;AAGrD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,UAAU,GACX,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EACL,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAGzF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG7F,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,QAAQ,EACR,UAAU,GACX,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAG7F,YAAY,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAGhE,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,KAAK,mCAAmC,GACzC,MAAM,6CAA6C,CAAC;AAGrD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,UAAU,GACX,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EACL,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAGzF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG7F,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,QAAQ,EACR,UAAU,GACX,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAG7F,YAAY,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAGhE,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAClE,YAAY,EACV,eAAe,EACf,eAAe,EACf,cAAc,IAAI,wBAAwB,EAC1C,aAAa,IAAI,uBAAuB,EACxC,aAAa,EACb,aAAa,GACd,MAAM,yBAAyB,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useTheme = exports.ThemeProvider = exports.CityViewWithReactFlow = exports.useCodeCityData = exports.MultiVersionCityBuilder = exports.drawFileTypeIcon = exports.getFileTypeIcon = exports.extractIconConfig = exports.mergeFileColorConfig = exports.devFileColorOverrides = exports.getFileColorMapping = exports.getDefaultFileColorConfig = exports.createFileColorHighlightLayers = exports.filterCityDataForMultipleDirectories = exports.filterCityDataForSubdirectory = exports.filterCityDataForSelectiveRender = exports.LayerIndex = exports.ArchitectureMapHighlightLayers = void 0;
3
+ exports.resetCamera = exports.FileCity3D = exports.useTheme = exports.ThemeProvider = exports.CityViewWithReactFlow = exports.useCodeCityData = exports.MultiVersionCityBuilder = exports.drawFileTypeIcon = exports.getFileTypeIcon = exports.extractIconConfig = exports.mergeFileColorConfig = exports.devFileColorOverrides = exports.getFileColorMapping = exports.getDefaultFileColorConfig = exports.createFileColorHighlightLayers = exports.filterCityDataForMultipleDirectories = exports.filterCityDataForSubdirectory = exports.filterCityDataForSelectiveRender = exports.LayerIndex = exports.ArchitectureMapHighlightLayers = void 0;
4
4
  // Main component export
5
5
  var ArchitectureMapHighlightLayers_1 = require("./components/ArchitectureMapHighlightLayers");
6
6
  Object.defineProperty(exports, "ArchitectureMapHighlightLayers", { enumerable: true, get: function () { return ArchitectureMapHighlightLayers_1.ArchitectureMapHighlightLayers; } });
@@ -39,3 +39,7 @@ Object.defineProperty(exports, "CityViewWithReactFlow", { enumerable: true, get:
39
39
  var industry_theme_1 = require("@principal-ade/industry-theme");
40
40
  Object.defineProperty(exports, "ThemeProvider", { enumerable: true, get: function () { return industry_theme_1.ThemeProvider; } });
41
41
  Object.defineProperty(exports, "useTheme", { enumerable: true, get: function () { return industry_theme_1.useTheme; } });
42
+ // 3D visualization component
43
+ var FileCity3D_1 = require("./components/FileCity3D");
44
+ Object.defineProperty(exports, "FileCity3D", { enumerable: true, get: function () { return FileCity3D_1.FileCity3D; } });
45
+ Object.defineProperty(exports, "resetCamera", { enumerable: true, get: function () { return FileCity3D_1.resetCamera; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/file-city-react",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "React components for File City visualization",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,9 +14,13 @@
14
14
  "dependencies": {
15
15
  "@principal-ade/industry-theme": "^0.1.3",
16
16
  "@principal-ai/alexandria-core-library": "^0.1.36",
17
- "@principal-ai/file-city-builder": "^0.4.0",
17
+ "@principal-ai/file-city-builder": "^0.4.5",
18
+ "@react-spring/three": "^9.7.5",
19
+ "@react-three/drei": "^10.0.6",
20
+ "@react-three/fiber": "^9.1.2",
18
21
  "lucide-react": "^0.563.0",
19
- "reactflow": "^11.11.4"
22
+ "reactflow": "^11.11.4",
23
+ "three": "^0.175.0"
20
24
  },
21
25
  "peerDependencies": {
22
26
  "react": "^19.0.0"
@@ -27,6 +31,7 @@
27
31
  "@storybook/react-vite": "^10.1.2",
28
32
  "@types/react": "^19.0.0",
29
33
  "@types/react-dom": "^19.2.3",
34
+ "@types/three": "^0.175.0",
30
35
  "react": "^19.1.1",
31
36
  "react-dom": "^19.1.1",
32
37
  "storybook": "^10.1.2",
@@ -1158,8 +1158,14 @@ function ArchitectureMapHighlightLayersInner({
1158
1158
 
1159
1159
  // Performance monitoring start available for debugging
1160
1160
 
1161
- const displayWidth = canvas.clientWidth || canvasSize.width;
1162
- const displayHeight = canvas.clientHeight || canvasSize.height;
1161
+ let displayWidth = canvas.clientWidth || canvasSize.width;
1162
+ let displayHeight = canvas.clientHeight || canvasSize.height;
1163
+
1164
+ // Cap canvas size if maxCanvasSize is set (prevents iOS Safari crashes)
1165
+ if (maxCanvasSize !== undefined) {
1166
+ displayWidth = Math.min(displayWidth, maxCanvasSize);
1167
+ displayHeight = Math.min(displayHeight, maxCanvasSize);
1168
+ }
1163
1169
 
1164
1170
  canvas.width = displayWidth;
1165
1171
  canvas.height = displayHeight;