@principal-ai/file-city-react 0.5.22 → 0.5.24

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.
@@ -21,6 +21,7 @@ import type {
21
21
  } from '@principal-ai/file-city-builder';
22
22
  import * as THREE from 'three';
23
23
  import type { ThreeElements } from '@react-three/fiber';
24
+ import { resolveVisualizationIntent } from '../../utils/visualizationResolution';
24
25
 
25
26
  // Extend JSX with Three.js elements
26
27
  declare module 'react' {
@@ -48,6 +49,10 @@ export interface HighlightLayer {
48
49
  items: HighlightItem[];
49
50
  /** Opacity for highlighted items (0-1) */
50
51
  opacity?: number;
52
+ /** Rendering priority (higher renders on top) */
53
+ priority?: number;
54
+ /** Border width in pixels */
55
+ borderWidth?: number;
51
56
  }
52
57
 
53
58
  export interface HighlightItem {
@@ -83,6 +88,40 @@ export interface AnimationConfig {
83
88
  /** Height scaling mode for buildings */
84
89
  export type HeightScaling = 'logarithmic' | 'linear';
85
90
 
91
+ /** Pattern for files that should render flat (e.g., lock files, generated files) */
92
+ export interface FlatPattern {
93
+ /** Glob-like pattern or regex to match file paths */
94
+ pattern: string | RegExp;
95
+ /** Height to use for matched files (default: 0.5) */
96
+ height?: number;
97
+ }
98
+
99
+ /** Default patterns for files that should render flat */
100
+ export const DEFAULT_FLAT_PATTERNS: FlatPattern[] = [
101
+ { pattern: /package-lock\.json$/ },
102
+ { pattern: /yarn\.lock$/ },
103
+ { pattern: /pnpm-lock\.yaml$/ },
104
+ { pattern: /composer\.lock$/ },
105
+ { pattern: /Gemfile\.lock$/ },
106
+ { pattern: /Cargo\.lock$/ },
107
+ { pattern: /poetry\.lock$/ },
108
+ { pattern: /\.lock$/ }, // Generic lock files
109
+ ];
110
+
111
+ /**
112
+ * Check if a file path matches any flat pattern.
113
+ * Returns the matched pattern's height or undefined if no match.
114
+ */
115
+ function matchFlatPattern(path: string, patterns: FlatPattern[]): number | undefined {
116
+ for (const { pattern, height } of patterns) {
117
+ const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
118
+ if (regex.test(path)) {
119
+ return height ?? 0.5; // Default flat height
120
+ }
121
+ }
122
+ return undefined;
123
+ }
124
+
86
125
  const DEFAULT_ANIMATION: AnimationConfig = {
87
126
  startFlat: false,
88
127
  autoStartDelay: 500,
@@ -100,8 +139,15 @@ const DEFAULT_ANIMATION: AnimationConfig = {
100
139
  function calculateBuildingHeight(
101
140
  building: CityBuilding,
102
141
  scaling: HeightScaling = 'logarithmic',
103
- linearScale: number = 0.05,
142
+ linearScale: number = 1,
143
+ flatPatterns: FlatPattern[] = [],
104
144
  ): number {
145
+ // Check if this file matches a flat pattern (e.g., lock files)
146
+ const flatHeight = matchFlatPattern(building.path, flatPatterns);
147
+ if (flatHeight !== undefined) {
148
+ return flatHeight;
149
+ }
150
+
105
151
  const minHeight = 2;
106
152
 
107
153
  // Use lineCount if available (any text file), otherwise fall back to size
@@ -375,6 +421,7 @@ interface InstancedBuildingsProps {
375
421
  animationConfig: AnimationConfig;
376
422
  heightScaling: HeightScaling;
377
423
  linearScale: number;
424
+ flatPatterns: FlatPattern[];
378
425
  staggerIndices: number[];
379
426
  focusDirectory: string | null;
380
427
  highlightLayers: HighlightLayer[];
@@ -398,6 +445,7 @@ function InstancedBuildings({
398
445
  animationConfig,
399
446
  heightScaling,
400
447
  linearScale,
448
+ flatPatterns,
401
449
  staggerIndices,
402
450
  focusDirectory,
403
451
  highlightLayers,
@@ -483,8 +531,10 @@ function InstancedBuildings({
483
531
  const buildingData = useMemo(() => {
484
532
  return buildings.map((building, index) => {
485
533
  const [width, , depth] = building.dimensions;
486
- const fullHeight = calculateBuildingHeight(building, heightScaling, linearScale);
487
- const color = getColorForFile(building);
534
+ const fullHeight = calculateBuildingHeight(building, heightScaling, linearScale, flatPatterns);
535
+ // Use highlight layer color if available, otherwise fall back to file config color
536
+ const highlight = getHighlightForPath(building.path, highlightLayers);
537
+ const color = highlight?.color ?? getColorForFile(building);
488
538
 
489
539
  const x = building.position.x - centerOffset.x;
490
540
  const z = building.position.z - centerOffset.z;
@@ -509,8 +559,10 @@ function InstancedBuildings({
509
559
  centerOffset,
510
560
  heightScaling,
511
561
  linearScale,
562
+ flatPatterns,
512
563
  staggerIndices,
513
564
  animationConfig.staggerDelay,
565
+ highlightLayers,
514
566
  ]);
515
567
 
516
568
  const minHeight = 0.3;
@@ -679,7 +731,7 @@ function InstancedBuildings({
679
731
  frustumCulled={false}
680
732
  >
681
733
  <boxGeometry args={[1, 1, 1]} />
682
- <meshStandardMaterial metalness={0.1} roughness={0.35} />
734
+ <meshBasicMaterial />
683
735
  </instancedMesh>
684
736
 
685
737
  {/* Building edge outlines */}
@@ -713,6 +765,7 @@ interface BuildingIconsProps {
713
765
  growProgress: number;
714
766
  heightScaling: HeightScaling;
715
767
  linearScale: number;
768
+ flatPatterns: FlatPattern[];
716
769
  highlightLayers: HighlightLayer[];
717
770
  isolationMode: IsolationMode;
718
771
  hasActiveHighlights: boolean;
@@ -826,6 +879,7 @@ function BuildingIcons({
826
879
  growProgress,
827
880
  heightScaling,
828
881
  linearScale,
882
+ flatPatterns,
829
883
  highlightLayers,
830
884
  isolationMode,
831
885
  hasActiveHighlights,
@@ -848,7 +902,7 @@ function BuildingIcons({
848
902
 
849
903
  if (shouldHide) return null;
850
904
 
851
- const fullHeight = calculateBuildingHeight(building, heightScaling, linearScale);
905
+ const fullHeight = calculateBuildingHeight(building, heightScaling, linearScale, flatPatterns);
852
906
  const targetHeight = shouldCollapse ? 0.5 : fullHeight;
853
907
 
854
908
  const x = building.position.x - centerOffset.x;
@@ -884,6 +938,7 @@ function BuildingIcons({
884
938
  hasActiveHighlights,
885
939
  heightScaling,
886
940
  linearScale,
941
+ flatPatterns,
887
942
  staggerIndices,
888
943
  staggerDelay,
889
944
  ]);
@@ -897,11 +952,10 @@ function BuildingIcons({
897
952
  const texture = getIconTexture(icon.name, icon.color || '#ffffff');
898
953
  if (!texture) return null;
899
954
 
900
- // Icon size based on building dimensions
901
- const [width] = building.dimensions;
902
- const baseSize = Math.max(width * 1.2, 8);
903
- const heightBoost = Math.min(targetHeight / 20, 3);
904
- const iconSize = (baseSize + heightBoost) * (icon.size || 1);
955
+ // Icon size based on building dimensions (matching 2D calculation)
956
+ const [width, , depth] = building.dimensions;
957
+ const minDimension = Math.min(width, depth);
958
+ const iconSize = minDimension * (icon.size || 0.6) * 1.7;
905
959
 
906
960
  const opacity = shouldDim && isolationMode === 'transparent' ? 0.3 : 1;
907
961
 
@@ -1139,6 +1193,7 @@ function AnimatedCamera({ citySize, isFlat, focusTarget, maxBuildingHeight = 0 }
1139
1193
 
1140
1194
  // Calculate camera height to fit city in viewport (for top-down view)
1141
1195
  // Formula: height = citySize / (2 * tan(fov/2) * min(1, aspect))
1196
+ // Padding factor adds space around the city to match 2D component
1142
1197
  const calculateFlatCameraHeight = useCallback(() => {
1143
1198
  const perspCam = camera as THREE.PerspectiveCamera;
1144
1199
  const fovRad = (perspCam.fov * Math.PI) / 180;
@@ -1146,9 +1201,10 @@ function AnimatedCamera({ citySize, isFlat, focusTarget, maxBuildingHeight = 0 }
1146
1201
  const aspect = perspCam.aspect || 1;
1147
1202
  // Use min(1, aspect) to handle both landscape and portrait viewports
1148
1203
  const effectiveAspect = Math.min(1, aspect);
1149
- // Add 10% padding to match 2D view
1150
- const padding = 1.1;
1151
- return (citySize * padding) / (2 * tanHalfFov * effectiveAspect);
1204
+ const baseHeight = citySize / (2 * tanHalfFov * effectiveAspect);
1205
+ // Add padding to match 2D component's default padding
1206
+ const paddingFactor = 1.08;
1207
+ return baseHeight * paddingFactor;
1152
1208
  }, [camera, citySize]);
1153
1209
 
1154
1210
  // Compute target camera position
@@ -1765,6 +1821,7 @@ interface CitySceneProps {
1765
1821
  isolationMode: IsolationMode;
1766
1822
  heightScaling: HeightScaling;
1767
1823
  linearScale: number;
1824
+ flatPatterns: FlatPattern[];
1768
1825
  focusDirectory: string | null;
1769
1826
  focusColor?: string | null;
1770
1827
  adaptCameraToBuildings?: boolean;
@@ -1782,6 +1839,7 @@ function CityScene({
1782
1839
  isolationMode,
1783
1840
  heightScaling,
1784
1841
  linearScale,
1842
+ flatPatterns,
1785
1843
  focusDirectory,
1786
1844
  focusColor,
1787
1845
  adaptCameraToBuildings = false,
@@ -1956,44 +2014,11 @@ function CityScene({
1956
2014
  return { x: centerX, z: centerZ, size };
1957
2015
  }
1958
2016
 
1959
- // Priority 2: highlight layers (only if no focusDirectory is pending)
1960
- // Don't focus on highlights if we're waiting for cameraFocusDirectory to catch up
1961
- if (!activeHighlights || focusDirectory) return null;
1962
-
1963
- const highlightedBuildings = cityData.buildings.filter(building => {
1964
- const highlight = getHighlightForPath(building.path, highlightLayers);
1965
- return highlight !== null;
1966
- });
1967
-
1968
- if (highlightedBuildings.length === 0) return null;
1969
-
1970
- let minX = Infinity,
1971
- maxX = -Infinity;
1972
- let minZ = Infinity,
1973
- maxZ = -Infinity;
1974
-
1975
- for (const building of highlightedBuildings) {
1976
- const x = building.position.x - centerOffset.x;
1977
- const z = building.position.z - centerOffset.z;
1978
- const [width, , depth] = building.dimensions;
1979
-
1980
- minX = Math.min(minX, x - width / 2);
1981
- maxX = Math.max(maxX, x + width / 2);
1982
- minZ = Math.min(minZ, z - depth / 2);
1983
- maxZ = Math.max(maxZ, z + depth / 2);
1984
- }
1985
-
1986
- const centerX = (minX + maxX) / 2;
1987
- const centerZ = (minZ + maxZ) / 2;
1988
- const size = Math.max(maxX - minX, maxZ - minZ);
1989
-
1990
- return { x: centerX, z: centerZ, size };
2017
+ // No auto-focus on highlights - camera only moves with explicit focusDirectory
2018
+ return null;
1991
2019
  }, [
1992
2020
  cameraFocusDirectory,
1993
- focusDirectory,
1994
- activeHighlights,
1995
2021
  cityData.buildings,
1996
- highlightLayers,
1997
2022
  centerOffset,
1998
2023
  isPathInDirectory,
1999
2024
  ]);
@@ -2098,6 +2123,7 @@ function CityScene({
2098
2123
  animationConfig={animationConfig}
2099
2124
  heightScaling={heightScaling}
2100
2125
  linearScale={linearScale}
2126
+ flatPatterns={flatPatterns}
2101
2127
  staggerIndices={staggerIndices}
2102
2128
  focusDirectory={buildingFocusDirectory}
2103
2129
  highlightLayers={highlightLayers}
@@ -2110,6 +2136,7 @@ function CityScene({
2110
2136
  growProgress={growProgress}
2111
2137
  heightScaling={heightScaling}
2112
2138
  linearScale={linearScale}
2139
+ flatPatterns={flatPatterns}
2113
2140
  highlightLayers={highlightLayers}
2114
2141
  isolationMode={isolationMode}
2115
2142
  hasActiveHighlights={activeHighlights}
@@ -2162,6 +2189,8 @@ export interface FileCity3DProps {
2162
2189
  heightScaling?: HeightScaling;
2163
2190
  /** Scale factor for linear mode (height per line, default 0.05) */
2164
2191
  linearScale?: number;
2192
+ /** Patterns for files that should render flat (e.g., lock files). Set to DEFAULT_FLAT_PATTERNS for common lock files, or [] to disable. */
2193
+ flatPatterns?: FlatPattern[];
2165
2194
  /** Directory path to focus on - buildings outside will collapse */
2166
2195
  focusDirectory?: string | null;
2167
2196
  /** Color to highlight the focused directory (hex color, e.g. "#3b82f6") */
@@ -2176,6 +2205,9 @@ export interface FileCity3DProps {
2176
2205
  selectedBuilding?: CityBuilding | null;
2177
2206
  /** When true, camera height adjusts based on tallest building when grown */
2178
2207
  adaptCameraToBuildings?: boolean;
2208
+
2209
+ /** Base file type color layers (resolved with highlightLayers) */
2210
+ fileColorLayers?: HighlightLayer[];
2179
2211
  }
2180
2212
 
2181
2213
  /**
@@ -2195,27 +2227,64 @@ export function FileCity3D({
2195
2227
  isGrown: externalIsGrown,
2196
2228
  onGrowChange,
2197
2229
  showControls = true,
2198
- highlightLayers = [],
2199
- isolationMode = 'transparent',
2230
+ highlightLayers: externalHighlightLayers,
2231
+ isolationMode: externalIsolationMode,
2200
2232
  dimOpacity: _dimOpacity = 0.15,
2201
2233
  isLoading = false,
2202
2234
  loadingMessage = 'Loading file city...',
2203
2235
  emptyMessage = 'No file tree data available',
2204
- heightScaling = 'logarithmic',
2205
- linearScale = 0.05,
2206
- focusDirectory = null,
2207
- focusColor = null,
2236
+ heightScaling = 'linear',
2237
+ linearScale = 1,
2238
+ flatPatterns = DEFAULT_FLAT_PATTERNS,
2239
+ focusDirectory: externalFocusDirectory,
2240
+ focusColor: externalFocusColor,
2208
2241
  onDirectorySelect: _onDirectorySelect,
2209
2242
  backgroundColor = '#0f172a',
2210
2243
  textColor = '#94a3b8',
2211
2244
  selectedBuilding = null,
2212
2245
  adaptCameraToBuildings = false,
2246
+ fileColorLayers,
2213
2247
  }: FileCity3DProps) {
2214
2248
  const [hoveredBuilding, setHoveredBuilding] = useState<CityBuilding | null>(null);
2215
2249
  const [internalIsGrown, setInternalIsGrown] = useState(false);
2216
2250
 
2217
2251
  const animationConfig = useMemo(() => ({ ...DEFAULT_ANIMATION, ...animation }), [animation]);
2218
2252
 
2253
+ // ============================================================================
2254
+ // Visualization Resolution
2255
+ // Always resolve: combines highlightLayers with fileColorLayers,
2256
+ // filtering fileColorLayers based on focus/highlight scope.
2257
+ // ============================================================================
2258
+ const resolved = useMemo(() => {
2259
+ // Cast to InputHighlightLayer[] for resolution - types are compatible at runtime
2260
+ const resolution = resolveVisualizationIntent({
2261
+ focusPath: externalFocusDirectory,
2262
+ focusColor: externalFocusColor,
2263
+ highlightLayers: (externalHighlightLayers ?? []) as Parameters<typeof resolveVisualizationIntent>[0]['highlightLayers'],
2264
+ fileColorLayers: (fileColorLayers ?? []) as Parameters<typeof resolveVisualizationIntent>[0]['fileColorLayers'],
2265
+ });
2266
+
2267
+ return {
2268
+ highlightLayers: resolution.highlightLayers as HighlightLayer[],
2269
+ focusDirectory: resolution.cameraFocusPath,
2270
+ focusColor: resolution.focusColor,
2271
+ // Use explicit isolation mode if provided, otherwise auto-determine
2272
+ isolationMode: externalIsolationMode ?? (resolution.shouldIsolate ? 'collapse' : 'none'),
2273
+ };
2274
+ }, [
2275
+ fileColorLayers,
2276
+ externalHighlightLayers,
2277
+ externalFocusDirectory,
2278
+ externalFocusColor,
2279
+ externalIsolationMode,
2280
+ ]);
2281
+
2282
+ // Use resolved values
2283
+ const highlightLayers = resolved.highlightLayers;
2284
+ const focusDirectory = resolved.focusDirectory;
2285
+ const focusColor = resolved.focusColor;
2286
+ const isolationMode = resolved.isolationMode as IsolationMode;
2287
+
2219
2288
  const isGrown = externalIsGrown !== undefined ? externalIsGrown : internalIsGrown;
2220
2289
  const setIsGrown = (value: boolean) => {
2221
2290
  setInternalIsGrown(value);
@@ -2303,6 +2372,7 @@ export function FileCity3D({
2303
2372
  >
2304
2373
  <Canvas
2305
2374
  shadows
2375
+ flat // Disables tone mapping for true colors
2306
2376
  style={{
2307
2377
  position: 'absolute',
2308
2378
  top: 0,
@@ -2323,6 +2393,7 @@ export function FileCity3D({
2323
2393
  isolationMode={isolationMode}
2324
2394
  heightScaling={heightScaling}
2325
2395
  linearScale={linearScale}
2396
+ flatPatterns={flatPatterns}
2326
2397
  focusDirectory={focusDirectory}
2327
2398
  focusColor={focusColor}
2328
2399
  adaptCameraToBuildings={adaptCameraToBuildings}
@@ -14,6 +14,7 @@ export {
14
14
  tiltCameraBy,
15
15
  moveCameraTo,
16
16
  setCameraTarget,
17
+ DEFAULT_FLAT_PATTERNS,
17
18
  } from './FileCity3D';
18
19
  export type {
19
20
  FileCity3DProps,
@@ -22,6 +23,7 @@ export type {
22
23
  HighlightItem,
23
24
  IsolationMode,
24
25
  HeightScaling,
26
+ FlatPattern,
25
27
  CityData,
26
28
  CityBuilding,
27
29
  CityDistrict,
package/src/index.ts CHANGED
@@ -72,7 +72,7 @@ export {
72
72
  export { ThemeProvider, useTheme } from '@principal-ade/industry-theme';
73
73
 
74
74
  // 3D visualization component
75
- export { FileCity3D, resetCamera } from './components/FileCity3D';
75
+ export { FileCity3D, resetCamera, DEFAULT_FLAT_PATTERNS } from './components/FileCity3D';
76
76
  export type {
77
77
  FileCity3DProps,
78
78
  AnimationConfig,
@@ -81,8 +81,17 @@ export type {
81
81
  HighlightItem,
82
82
  IsolationMode,
83
83
  HeightScaling,
84
+ FlatPattern,
84
85
  } from './components/FileCity3D';
85
86
 
86
87
  // Re-export HighlightLayer from FileCity3D with distinct name to avoid conflict
87
88
  // with the 2D HighlightLayer from drawLayeredBuildings
88
89
  export type { HighlightLayer as FileCity3DHL } from './components/FileCity3D';
90
+
91
+ // Visualization resolution utilities
92
+ // See docs/VISUALIZATION_STATE_RESOLUTION.md for documentation
93
+ export { resolveVisualizationIntent } from './utils/visualizationResolution';
94
+ export type {
95
+ VisualizationIntent,
96
+ ResolvedVisualizationState,
97
+ } from './utils/visualizationResolution';