@principal-ai/file-city-react 0.5.16 → 0.5.17

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.
@@ -168,6 +168,8 @@ export interface FileCity3DProps {
168
168
  textColor?: string;
169
169
  /** Currently selected building (controlled by host) */
170
170
  selectedBuilding?: CityBuilding | null;
171
+ /** When true, camera height adjusts based on tallest building when grown */
172
+ adaptCameraToBuildings?: boolean;
171
173
  }
172
174
  /**
173
175
  * FileCity3D - 3D visualization of codebase structure
@@ -175,6 +177,6 @@ export interface FileCity3DProps {
175
177
  * Renders CityData as an interactive 3D city where buildings represent files
176
178
  * and their height corresponds to line count or file size.
177
179
  */
178
- export declare function FileCity3D({ cityData, width, height, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls, highlightLayers, isolationMode, dimOpacity: _dimOpacity, isLoading, loadingMessage, emptyMessage, heightScaling, linearScale, focusDirectory, focusColor, onDirectorySelect: _onDirectorySelect, backgroundColor, textColor, selectedBuilding, }: FileCity3DProps): import("react/jsx-runtime").JSX.Element;
180
+ export declare function FileCity3D({ cityData, width, height, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls, highlightLayers, isolationMode, dimOpacity: _dimOpacity, isLoading, loadingMessage, emptyMessage, heightScaling, linearScale, focusDirectory, focusColor, onDirectorySelect: _onDirectorySelect, backgroundColor, textColor, selectedBuilding, adaptCameraToBuildings, }: FileCity3DProps): import("react/jsx-runtime").JSX.Element;
179
181
  export default FileCity3D;
180
182
  //# sourceMappingURL=FileCity3D.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAA4D,MAAM,OAAO,CAAC;AAMjF,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;AAs4BrD,MAAM,WAAW,aAAa;IAC5B,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAmBD,wBAAgB,WAAW,SAE1B;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,QAE/D;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,QAEvF;AAED;;GAEG;AACH,wBAAgB,eAAe;OA9BA,MAAM;OAAK,MAAM;OAAK,MAAM;SAgC1D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,gBAAgB,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,EAC9D,OAAO,CAAC,EAAE,aAAa,QAGxB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,QAEtE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,EAChD,OAAO,CAAC,EAAE,aAAa,QAGxB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,QAEpE;AAED,wBAAgB,iBAAiB;OAhFA,MAAM;OAAK,MAAM;OAAK,MAAM;SAkF5D;AAED;;;GAGG;AACH,wBAAgB,cAAc,kBAE7B;AAED;;;GAGG;AACH,wBAAgB,aAAa,kBAE5B;AAq8BD,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,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,0DAA0D;IAC1D,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACvD,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACvC,4EAA4E;IAC5E,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;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,UAAU,EAAE,WAAkB,EAC9B,SAAiB,EACjB,cAAuC,EACvC,YAA4C,EAC5C,aAA6B,EAC7B,WAAkB,EAClB,cAAqB,EACrB,UAAiB,EACjB,iBAAiB,EAAE,kBAAkB,EACrC,eAA2B,EAC3B,SAAqB,EACrB,gBAAuB,EACvB,sBAA8B,GAC/B,EAAE,eAAe,2CA4HjB;AAED,eAAe,UAAU,CAAC"}
@@ -623,7 +623,7 @@ export function getCameraAngle() {
623
623
  export function getCameraTilt() {
624
624
  return cameraApi?.getCurrentTilt() ?? null;
625
625
  }
626
- function AnimatedCamera({ citySize, isFlat: _isFlat, focusTarget }) {
626
+ function AnimatedCamera({ citySize, isFlat, focusTarget, maxBuildingHeight = 0 }) {
627
627
  const { camera } = useThree();
628
628
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
629
629
  const controlsRef = useRef(null);
@@ -632,6 +632,7 @@ function AnimatedCamera({ citySize, isFlat: _isFlat, focusTarget }) {
632
632
  const hasAppliedInitial = useRef(false);
633
633
  const frameCount = useRef(0);
634
634
  // Compute target camera position
635
+ // When flat, use a more top-down view; when grown, use an angled view
635
636
  const targetPos = useMemo(() => {
636
637
  if (focusTarget) {
637
638
  const distance = Math.max(focusTarget.size * 2, 50);
@@ -645,9 +646,15 @@ function AnimatedCamera({ citySize, isFlat: _isFlat, focusTarget }) {
645
646
  targetZ: focusTarget.z,
646
647
  };
647
648
  }
648
- // Default overview
649
- const targetHeight = citySize * 1.1;
650
- const targetZ = citySize * 1.3;
649
+ // Default overview - adjust angle based on flat/grown state
650
+ // Flat: directly overhead (90 degrees, looking straight down)
651
+ // Grown: angled view to see building heights (optionally based on max building height)
652
+ const baseHeight = citySize * 1.1;
653
+ const buildingAwareHeight = maxBuildingHeight > 0
654
+ ? Math.max(baseHeight, maxBuildingHeight * 2.5)
655
+ : baseHeight;
656
+ const targetHeight = isFlat ? citySize * 2.0 : buildingAwareHeight;
657
+ const targetZ = isFlat ? 0.001 : citySize * 1.3; // Near-zero for top-down (tiny offset to avoid gimbal lock)
651
658
  return {
652
659
  x: 0,
653
660
  y: targetHeight,
@@ -656,7 +663,7 @@ function AnimatedCamera({ citySize, isFlat: _isFlat, focusTarget }) {
656
663
  targetY: 0,
657
664
  targetZ: 0,
658
665
  };
659
- }, [focusTarget, citySize]);
666
+ }, [focusTarget, citySize, isFlat, maxBuildingHeight]);
660
667
  // Spring animation for camera movement
661
668
  const [{ camX, camY, camZ, lookX, lookY, lookZ }, api] = useSpring(() => ({
662
669
  camX: targetPos.x,
@@ -1074,12 +1081,18 @@ function ControlsOverlay({ isFlat, onToggle, onResetCamera }) {
1074
1081
  gap: 8,
1075
1082
  }, children: [_jsx("button", { onClick: onResetCamera, style: buttonStyle, children: "Reset View" }), _jsx("button", { onClick: onToggle, style: buttonStyle, children: isFlat ? 'Grow to 3D' : 'Flatten to 2D' })] }));
1076
1083
  }
1077
- function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding, selectedBuilding, growProgress, animationConfig, highlightLayers, isolationMode, heightScaling, linearScale, focusDirectory, focusColor, }) {
1084
+ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding, selectedBuilding, growProgress, animationConfig, highlightLayers, isolationMode, heightScaling, linearScale, focusDirectory, focusColor, adaptCameraToBuildings = false, }) {
1078
1085
  const centerOffset = useMemo(() => ({
1079
1086
  x: (cityData.bounds.minX + cityData.bounds.maxX) / 2,
1080
1087
  z: (cityData.bounds.minZ + cityData.bounds.maxZ) / 2,
1081
1088
  }), [cityData.bounds]);
1082
1089
  const citySize = Math.max(cityData.bounds.maxX - cityData.bounds.minX, cityData.bounds.maxZ - cityData.bounds.minZ);
1090
+ // Calculate max building height for camera positioning (when adaptCameraToBuildings is true)
1091
+ const maxBuildingHeight = useMemo(() => {
1092
+ if (!adaptCameraToBuildings)
1093
+ return 0;
1094
+ return Math.max(...cityData.buildings.map(b => b.dimensions[1]), 0);
1095
+ }, [adaptCameraToBuildings, cityData.buildings]);
1083
1096
  const activeHighlights = useMemo(() => hasActiveHighlights(highlightLayers), [highlightLayers]);
1084
1097
  // Helper to check if a path is inside a directory
1085
1098
  const isPathInDirectory = useCallback((path, directory) => {
@@ -1262,7 +1275,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
1262
1275
  const tension = animationConfig.tension || 120;
1263
1276
  const friction = animationConfig.friction || 14;
1264
1277
  const springDuration = Math.sqrt(1 / (tension * 0.001)) * friction * 20;
1265
- 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 => {
1278
+ return (_jsxs(_Fragment, { children: [_jsx(AnimatedCamera, { citySize: citySize, isFlat: growProgress === 0, focusTarget: focusTarget, maxBuildingHeight: maxBuildingHeight }), _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 => {
1266
1279
  // Check if district matches focusDirectory
1267
1280
  const isFocused = buildingFocusDirectory
1268
1281
  ? district.path === buildingFocusDirectory
@@ -1293,7 +1306,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
1293
1306
  * Renders CityData as an interactive 3D city where buildings represent files
1294
1307
  * and their height corresponds to line count or file size.
1295
1308
  */
1296
- export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls = true, highlightLayers = [], isolationMode = 'transparent', dimOpacity: _dimOpacity = 0.15, isLoading = false, loadingMessage = 'Loading file city...', emptyMessage = 'No file tree data available', heightScaling = 'logarithmic', linearScale = 0.05, focusDirectory = null, focusColor = null, onDirectorySelect: _onDirectorySelect, backgroundColor = '#0f172a', textColor = '#94a3b8', selectedBuilding = null, }) {
1309
+ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls = true, highlightLayers = [], isolationMode = 'transparent', dimOpacity: _dimOpacity = 0.15, isLoading = false, loadingMessage = 'Loading file city...', emptyMessage = 'No file tree data available', heightScaling = 'logarithmic', linearScale = 0.05, focusDirectory = null, focusColor = null, onDirectorySelect: _onDirectorySelect, backgroundColor = '#0f172a', textColor = '#94a3b8', selectedBuilding = null, adaptCameraToBuildings = false, }) {
1297
1310
  const [hoveredBuilding, setHoveredBuilding] = useState(null);
1298
1311
  const [internalIsGrown, setInternalIsGrown] = useState(false);
1299
1312
  const animationConfig = useMemo(() => ({ ...DEFAULT_ANIMATION, ...animation }), [animation]);
@@ -1364,6 +1377,6 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
1364
1377
  left: 0,
1365
1378
  width: '100%',
1366
1379
  height: '100%',
1367
- }, children: _jsx(CityScene, { cityData: cityData, onBuildingHover: setHoveredBuilding, onBuildingClick: onBuildingClick, hoveredBuilding: hoveredBuilding, selectedBuilding: selectedBuilding, growProgress: growProgress, animationConfig: animationConfig, highlightLayers: highlightLayers, isolationMode: isolationMode, heightScaling: heightScaling, linearScale: linearScale, focusDirectory: focusDirectory, focusColor: focusColor }) }), _jsx(InfoPanel, { building: selectedBuilding || hoveredBuilding }), showControls && (_jsx(ControlsOverlay, { isFlat: !isGrown, onToggle: handleToggle, onResetCamera: resetCamera }))] }));
1380
+ }, children: _jsx(CityScene, { cityData: cityData, onBuildingHover: setHoveredBuilding, onBuildingClick: onBuildingClick, hoveredBuilding: hoveredBuilding, selectedBuilding: selectedBuilding, growProgress: growProgress, animationConfig: animationConfig, highlightLayers: highlightLayers, isolationMode: isolationMode, heightScaling: heightScaling, linearScale: linearScale, focusDirectory: focusDirectory, focusColor: focusColor, adaptCameraToBuildings: adaptCameraToBuildings }) }), _jsx(InfoPanel, { building: selectedBuilding || hoveredBuilding }), showControls && (_jsx(ControlsOverlay, { isFlat: !isGrown, onToggle: handleToggle, onResetCamera: resetCamera }))] }));
1368
1381
  }
1369
1382
  export default FileCity3D;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/file-city-react",
3
- "version": "0.5.16",
3
+ "version": "0.5.17",
4
4
  "type": "module",
5
5
  "description": "React components for File City visualization",
6
6
  "main": "dist/index.js",
@@ -979,6 +979,7 @@ interface AnimatedCameraProps {
979
979
  citySize: number;
980
980
  isFlat: boolean;
981
981
  focusTarget?: FocusTarget | null;
982
+ maxBuildingHeight?: number;
982
983
  }
983
984
 
984
985
  // Camera rotation options
@@ -1096,7 +1097,7 @@ export function getCameraTilt() {
1096
1097
  return cameraApi?.getCurrentTilt() ?? null;
1097
1098
  }
1098
1099
 
1099
- function AnimatedCamera({ citySize, isFlat: _isFlat, focusTarget }: AnimatedCameraProps) {
1100
+ function AnimatedCamera({ citySize, isFlat, focusTarget, maxBuildingHeight = 0 }: AnimatedCameraProps) {
1100
1101
  const { camera } = useThree();
1101
1102
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1102
1103
  const controlsRef = useRef<any>(null);
@@ -1106,6 +1107,7 @@ function AnimatedCamera({ citySize, isFlat: _isFlat, focusTarget }: AnimatedCame
1106
1107
  const frameCount = useRef(0);
1107
1108
 
1108
1109
  // Compute target camera position
1110
+ // When flat, use a more top-down view; when grown, use an angled view
1109
1111
  const targetPos = useMemo(() => {
1110
1112
  if (focusTarget) {
1111
1113
  const distance = Math.max(focusTarget.size * 2, 50);
@@ -1119,9 +1121,15 @@ function AnimatedCamera({ citySize, isFlat: _isFlat, focusTarget }: AnimatedCame
1119
1121
  targetZ: focusTarget.z,
1120
1122
  };
1121
1123
  }
1122
- // Default overview
1123
- const targetHeight = citySize * 1.1;
1124
- const targetZ = citySize * 1.3;
1124
+ // Default overview - adjust angle based on flat/grown state
1125
+ // Flat: directly overhead (90 degrees, looking straight down)
1126
+ // Grown: angled view to see building heights (optionally based on max building height)
1127
+ const baseHeight = citySize * 1.1;
1128
+ const buildingAwareHeight = maxBuildingHeight > 0
1129
+ ? Math.max(baseHeight, maxBuildingHeight * 2.5)
1130
+ : baseHeight;
1131
+ const targetHeight = isFlat ? citySize * 2.0 : buildingAwareHeight;
1132
+ const targetZ = isFlat ? 0.001 : citySize * 1.3; // Near-zero for top-down (tiny offset to avoid gimbal lock)
1125
1133
  return {
1126
1134
  x: 0,
1127
1135
  y: targetHeight,
@@ -1130,7 +1138,7 @@ function AnimatedCamera({ citySize, isFlat: _isFlat, focusTarget }: AnimatedCame
1130
1138
  targetY: 0,
1131
1139
  targetZ: 0,
1132
1140
  };
1133
- }, [focusTarget, citySize]);
1141
+ }, [focusTarget, citySize, isFlat, maxBuildingHeight]);
1134
1142
 
1135
1143
  // Spring animation for camera movement
1136
1144
  const [{ camX, camY, camZ, lookX, lookY, lookZ }, api] = useSpring(() => ({
@@ -1695,6 +1703,7 @@ interface CitySceneProps {
1695
1703
  linearScale: number;
1696
1704
  focusDirectory: string | null;
1697
1705
  focusColor?: string | null;
1706
+ adaptCameraToBuildings?: boolean;
1698
1707
  }
1699
1708
 
1700
1709
  function CityScene({
@@ -1711,6 +1720,7 @@ function CityScene({
1711
1720
  linearScale,
1712
1721
  focusDirectory,
1713
1722
  focusColor,
1723
+ adaptCameraToBuildings = false,
1714
1724
  }: CitySceneProps) {
1715
1725
  const centerOffset = useMemo(
1716
1726
  () => ({
@@ -1725,6 +1735,12 @@ function CityScene({
1725
1735
  cityData.bounds.maxZ - cityData.bounds.minZ,
1726
1736
  );
1727
1737
 
1738
+ // Calculate max building height for camera positioning (when adaptCameraToBuildings is true)
1739
+ const maxBuildingHeight = useMemo(() => {
1740
+ if (!adaptCameraToBuildings) return 0;
1741
+ return Math.max(...cityData.buildings.map(b => b.dimensions[1]), 0);
1742
+ }, [adaptCameraToBuildings, cityData.buildings]);
1743
+
1728
1744
  const activeHighlights = useMemo(() => hasActiveHighlights(highlightLayers), [highlightLayers]);
1729
1745
 
1730
1746
  // Helper to check if a path is inside a directory
@@ -1956,7 +1972,7 @@ function CityScene({
1956
1972
 
1957
1973
  return (
1958
1974
  <>
1959
- <AnimatedCamera citySize={citySize} isFlat={growProgress === 0} focusTarget={focusTarget} />
1975
+ <AnimatedCamera citySize={citySize} isFlat={growProgress === 0} focusTarget={focusTarget} maxBuildingHeight={maxBuildingHeight} />
1960
1976
 
1961
1977
  <ambientLight intensity={1.2} />
1962
1978
  <hemisphereLight args={['#ddeeff', '#667788', 0.8]} position={[0, citySize, 0]} />
@@ -2093,6 +2109,8 @@ export interface FileCity3DProps {
2093
2109
  textColor?: string;
2094
2110
  /** Currently selected building (controlled by host) */
2095
2111
  selectedBuilding?: CityBuilding | null;
2112
+ /** When true, camera height adjusts based on tallest building when grown */
2113
+ adaptCameraToBuildings?: boolean;
2096
2114
  }
2097
2115
 
2098
2116
  /**
@@ -2126,6 +2144,7 @@ export function FileCity3D({
2126
2144
  backgroundColor = '#0f172a',
2127
2145
  textColor = '#94a3b8',
2128
2146
  selectedBuilding = null,
2147
+ adaptCameraToBuildings = false,
2129
2148
  }: FileCity3DProps) {
2130
2149
  const [hoveredBuilding, setHoveredBuilding] = useState<CityBuilding | null>(null);
2131
2150
  const [internalIsGrown, setInternalIsGrown] = useState(false);
@@ -2241,6 +2260,7 @@ export function FileCity3D({
2241
2260
  linearScale={linearScale}
2242
2261
  focusDirectory={focusDirectory}
2243
2262
  focusColor={focusColor}
2263
+ adaptCameraToBuildings={adaptCameraToBuildings}
2244
2264
  />
2245
2265
  </Canvas>
2246
2266
  <InfoPanel building={selectedBuilding || hoveredBuilding} />
@@ -2001,3 +2001,134 @@ export const CameraControls: Story = {
2001
2001
  },
2002
2002
  },
2003
2003
  };
2004
+
2005
+ /**
2006
+ * 2D to 3D Transition - Tests the camera angle adjustment when transitioning from flat to grown
2007
+ * Camera starts top-down when flat, then animates to an angled view when buildings grow
2008
+ */
2009
+ const FlatToGrownTransitionTemplate: React.FC = () => {
2010
+ const [isGrown, setIsGrown] = React.useState(false);
2011
+ const [autoTransition, setAutoTransition] = React.useState(false);
2012
+ const [adaptCamera, setAdaptCamera] = React.useState(true);
2013
+
2014
+ // Auto-transition effect (simulates the CodeCityPanel behavior)
2015
+ React.useEffect(() => {
2016
+ if (!autoTransition) return;
2017
+ setIsGrown(false);
2018
+ const timer = setTimeout(() => {
2019
+ setIsGrown(true);
2020
+ }, 600);
2021
+ return () => clearTimeout(timer);
2022
+ }, [autoTransition]);
2023
+
2024
+ return (
2025
+ <div style={{ height: '100vh', display: 'flex', flexDirection: 'column', position: 'relative' }}>
2026
+ {/* 3D City */}
2027
+ <FileCity3D
2028
+ cityData={sampleCityData}
2029
+ height="100%"
2030
+ heightScaling="linear"
2031
+ linearScale={0.5}
2032
+ isGrown={isGrown}
2033
+ adaptCameraToBuildings={adaptCamera}
2034
+ animation={{
2035
+ startFlat: true,
2036
+ autoStartDelay: null, // External control
2037
+ staggerDelay: 15,
2038
+ tension: 120,
2039
+ friction: 14,
2040
+ }}
2041
+ showControls={true}
2042
+ />
2043
+
2044
+ {/* Control panel */}
2045
+ <div
2046
+ style={{
2047
+ position: 'absolute',
2048
+ bottom: 0,
2049
+ left: 0,
2050
+ right: 0,
2051
+ zIndex: 100,
2052
+ background: 'rgba(15, 23, 42, 0.95)',
2053
+ borderTop: '1px solid #334155',
2054
+ padding: '16px 24px',
2055
+ }}
2056
+ >
2057
+ <div style={{ display: 'flex', gap: '16px', alignItems: 'center', justifyContent: 'center' }}>
2058
+ <div style={{ color: '#94a3b8', fontSize: '14px' }}>
2059
+ State: <strong style={{ color: isGrown ? '#22c55e' : '#f59e0b' }}>{isGrown ? 'GROWN' : 'FLAT'}</strong>
2060
+ </div>
2061
+ <button
2062
+ onClick={() => setIsGrown(false)}
2063
+ style={{
2064
+ padding: '8px 16px',
2065
+ background: !isGrown ? '#3b82f6' : '#1e293b',
2066
+ color: 'white',
2067
+ border: '1px solid #334155',
2068
+ borderRadius: '6px',
2069
+ cursor: 'pointer',
2070
+ }}
2071
+ >
2072
+ Flatten (2D)
2073
+ </button>
2074
+ <button
2075
+ onClick={() => setIsGrown(true)}
2076
+ style={{
2077
+ padding: '8px 16px',
2078
+ background: isGrown ? '#3b82f6' : '#1e293b',
2079
+ color: 'white',
2080
+ border: '1px solid #334155',
2081
+ borderRadius: '6px',
2082
+ cursor: 'pointer',
2083
+ }}
2084
+ >
2085
+ Grow (3D)
2086
+ </button>
2087
+ <button
2088
+ onClick={() => {
2089
+ setAutoTransition(false);
2090
+ setTimeout(() => setAutoTransition(true), 10);
2091
+ }}
2092
+ style={{
2093
+ padding: '8px 16px',
2094
+ background: '#7c3aed',
2095
+ color: 'white',
2096
+ border: '1px solid #334155',
2097
+ borderRadius: '6px',
2098
+ cursor: 'pointer',
2099
+ }}
2100
+ >
2101
+ Simulate 2D→3D Transition
2102
+ </button>
2103
+ <label style={{ display: 'flex', alignItems: 'center', gap: '6px', color: '#94a3b8', fontSize: '14px', cursor: 'pointer' }}>
2104
+ <input
2105
+ type="checkbox"
2106
+ checked={adaptCamera}
2107
+ onChange={(e) => setAdaptCamera(e.target.checked)}
2108
+ style={{ cursor: 'pointer' }}
2109
+ />
2110
+ Adapt to building heights
2111
+ </label>
2112
+ </div>
2113
+ <div style={{ color: '#64748b', fontSize: '12px', textAlign: 'center', marginTop: '8px' }}>
2114
+ Camera should start top-down when flat, then animate to angled view when grown.
2115
+ {adaptCamera && ' Camera height will adjust based on tallest building.'}
2116
+ </div>
2117
+ </div>
2118
+ </div>
2119
+ );
2120
+ };
2121
+
2122
+ export const FlatToGrownTransition: Story = {
2123
+ render: () => <FlatToGrownTransitionTemplate />,
2124
+ parameters: {
2125
+ docs: {
2126
+ description: {
2127
+ story:
2128
+ 'Tests the camera angle adjustment when transitioning between flat (2D) and grown (3D) states. ' +
2129
+ 'The camera should start with a top-down view when buildings are flat, then animate to an angled view when buildings grow. ' +
2130
+ 'Use "Simulate 2D→3D Transition" to test the full transition sequence as it happens in CodeCityPanel.',
2131
+ },
2132
+ },
2133
+ },
2134
+ };