@principal-ai/file-city-react 0.5.34 → 0.5.35
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.
|
@@ -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":"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"}
|
|
@@ -666,95 +666,51 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
666
666
|
const frameCount = useRef(0);
|
|
667
667
|
const hasNotifiedReady = useRef(false);
|
|
668
668
|
const prevIsFlatRef = useRef(isFlat); // Track previous isFlat to detect actual state changes
|
|
669
|
-
|
|
670
|
-
// Calculate camera height to fit city in viewport (for top-down view)
|
|
669
|
+
// Helper to calculate flat camera height with known FOV (50) and aspect ratio
|
|
671
670
|
// Formula: height = citySize / (2 * tan(fov/2) * min(1, aspect))
|
|
672
671
|
// Padding factor adds space around the city to match 2D component
|
|
673
|
-
const calculateFlatCameraHeight = useCallback(() => {
|
|
674
|
-
const
|
|
675
|
-
const fovRad = (
|
|
672
|
+
const calculateFlatCameraHeight = useCallback((aspect) => {
|
|
673
|
+
const fov = 50; // Known FOV that will be set on PerspectiveCamera
|
|
674
|
+
const fovRad = (fov * Math.PI) / 180;
|
|
676
675
|
const tanHalfFov = Math.tan(fovRad / 2);
|
|
677
|
-
const aspect = perspCam.aspect || 1;
|
|
678
676
|
// Use min(1, aspect) to handle both landscape and portrait viewports
|
|
679
677
|
const effectiveAspect = Math.min(1, aspect);
|
|
680
678
|
const baseHeight = citySize / (2 * tanHalfFov * effectiveAspect);
|
|
681
679
|
// Add padding to match 2D component's default padding
|
|
682
680
|
const paddingFactor = 1.08;
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
});
|
|
692
|
-
return result;
|
|
693
|
-
}, [camera, citySize]);
|
|
694
|
-
// Compute target camera position
|
|
695
|
-
// When flat, always use top-down view (ignore focusTarget)
|
|
696
|
-
// When grown, use focusTarget if available, otherwise angled overview
|
|
697
|
-
const targetPos = useMemo(() => {
|
|
698
|
-
// Flat state: always top-down, ignore any focus
|
|
699
|
-
// Height calculated mathematically to match 2D view zoom level
|
|
700
|
-
if (isFlat) {
|
|
701
|
-
return {
|
|
702
|
-
x: 0,
|
|
703
|
-
y: calculateFlatCameraHeight(),
|
|
704
|
-
z: 0.001, // Near-zero for top-down (tiny offset to avoid gimbal lock)
|
|
705
|
-
targetX: 0,
|
|
706
|
-
targetY: 0,
|
|
707
|
-
targetZ: 0,
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
// Grown state: use focusTarget if available
|
|
711
|
-
if (focusTarget) {
|
|
712
|
-
const distance = Math.max(focusTarget.size * 2, 50);
|
|
713
|
-
const height = Math.max(focusTarget.size * 1.5, 40);
|
|
714
|
-
return {
|
|
715
|
-
x: focusTarget.x,
|
|
716
|
-
y: height,
|
|
717
|
-
z: focusTarget.z + distance,
|
|
718
|
-
targetX: focusTarget.x,
|
|
719
|
-
targetY: 0,
|
|
720
|
-
targetZ: focusTarget.z,
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
// Grown state without focus: angled overview
|
|
724
|
-
const baseHeight = citySize * 1.1;
|
|
725
|
-
const buildingAwareHeight = maxBuildingHeight > 0
|
|
726
|
-
? Math.max(baseHeight, maxBuildingHeight * 2.5)
|
|
727
|
-
: baseHeight;
|
|
681
|
+
return baseHeight * paddingFactor;
|
|
682
|
+
}, [citySize]);
|
|
683
|
+
// Calculate initial 2D position (component always starts in 2D mode)
|
|
684
|
+
// We need aspect ratio from the camera, but we'll use a default until Frame 1
|
|
685
|
+
const getInitial2DPosition = useCallback(() => {
|
|
686
|
+
const perspCam = camera;
|
|
687
|
+
const aspect = perspCam.aspect || 1;
|
|
688
|
+
const height = calculateFlatCameraHeight(aspect);
|
|
728
689
|
return {
|
|
729
690
|
x: 0,
|
|
730
|
-
y:
|
|
731
|
-
z:
|
|
691
|
+
y: height,
|
|
692
|
+
z: 0.001, // Near-zero for top-down (tiny offset to avoid gimbal lock)
|
|
732
693
|
targetX: 0,
|
|
733
694
|
targetY: 0,
|
|
734
695
|
targetZ: 0,
|
|
735
696
|
};
|
|
736
|
-
}, [
|
|
737
|
-
// Freeze initial camera position on Frame 1 (not during render)
|
|
738
|
-
// This ensures we capture the correct calculation after canvas is initialized
|
|
739
|
-
const initialPosRef = useRef(null);
|
|
697
|
+
}, [camera, calculateFlatCameraHeight]);
|
|
740
698
|
// Spring animation for camera movement
|
|
741
|
-
// Initialize
|
|
699
|
+
// Initialize with correct 2D position from the start
|
|
742
700
|
const [{ camX, camY, camZ, lookX, lookY, lookZ }, api] = useSpring(() => {
|
|
743
|
-
|
|
701
|
+
// Calculate initial position with default aspect ratio
|
|
702
|
+
// This will be corrected in Frame 1 if aspect is different
|
|
703
|
+
const initialHeight = calculateFlatCameraHeight(1);
|
|
704
|
+
console.log('[Spring init] Initializing with 2D position, height:', initialHeight);
|
|
744
705
|
return {
|
|
745
706
|
camX: 0,
|
|
746
|
-
camY:
|
|
747
|
-
camZ: 0,
|
|
707
|
+
camY: initialHeight,
|
|
708
|
+
camZ: 0.001,
|
|
748
709
|
lookX: 0,
|
|
749
710
|
lookY: 0,
|
|
750
711
|
lookZ: 0,
|
|
751
712
|
config: { tension: 60, friction: 20 },
|
|
752
713
|
onStart: () => {
|
|
753
|
-
// Block animations during initial sync in Frame 3
|
|
754
|
-
if (isSyncingInitial.current) {
|
|
755
|
-
console.log('[Spring onStart] Blocked - syncing initial position');
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
714
|
// Only allow animations after initial setup is complete
|
|
759
715
|
if (hasAppliedInitial.current) {
|
|
760
716
|
console.log('[Spring onStart] Animation starting - camY:', camY.get());
|
|
@@ -797,37 +753,26 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
797
753
|
const orbitParamsRef = useRef(null);
|
|
798
754
|
// Track tilt parameters during vertical rotation
|
|
799
755
|
const tiltParamsRef = useRef(null);
|
|
800
|
-
// When isFlat changes
|
|
801
|
-
//
|
|
802
|
-
// from aspect ratio changes or other recalculations
|
|
756
|
+
// When isFlat changes from true to false, animate to 3D view
|
|
757
|
+
// Component always starts in 2D, so we only animate the 2D→3D transition
|
|
803
758
|
useEffect(() => {
|
|
804
|
-
console.log('[useEffect]
|
|
805
|
-
console.log('[useEffect] Dependency check - focusTarget:', focusTarget, 'citySize:', citySize, 'maxBuildingHeight:', maxBuildingHeight);
|
|
759
|
+
console.log('[useEffect] isFlat:', isFlat, 'prevIsFlat:', prevIsFlatRef.current, 'hasAppliedInitial:', hasAppliedInitial.current);
|
|
806
760
|
// Skip until camera is initialized
|
|
807
|
-
if (!hasAppliedInitial.current
|
|
761
|
+
if (!hasAppliedInitial.current) {
|
|
808
762
|
console.log('[useEffect] Skipping - not initialized yet');
|
|
809
763
|
return;
|
|
810
764
|
}
|
|
811
|
-
// Only animate if isFlat
|
|
765
|
+
// Only animate if isFlat changed from true to false (2D → 3D transition)
|
|
812
766
|
const isFlatChanged = prevIsFlatRef.current !== isFlat;
|
|
813
767
|
if (!isFlatChanged) {
|
|
814
|
-
|
|
815
|
-
// Use initialPosRef to prevent updates from targetPos recalculations
|
|
816
|
-
console.log('[useEffect] No isFlat change - skipping (prevIsFlat:', prevIsFlatRef.current, 'isFlat:', isFlat, ')');
|
|
768
|
+
console.log('[useEffect] No isFlat change - skipping');
|
|
817
769
|
return;
|
|
818
770
|
}
|
|
819
771
|
console.log('[useEffect] isFlat changed from', prevIsFlatRef.current, 'to', isFlat, '- animating transition');
|
|
820
772
|
prevIsFlatRef.current = isFlat;
|
|
821
|
-
//
|
|
773
|
+
// Calculate target position for 3D view
|
|
822
774
|
const newPos = isFlat
|
|
823
|
-
?
|
|
824
|
-
x: 0,
|
|
825
|
-
y: calculateFlatCameraHeight(),
|
|
826
|
-
z: 0.001,
|
|
827
|
-
targetX: 0,
|
|
828
|
-
targetY: 0,
|
|
829
|
-
targetZ: 0,
|
|
830
|
-
}
|
|
775
|
+
? getInitial2DPosition() // Going back to 2D
|
|
831
776
|
: focusTarget
|
|
832
777
|
? {
|
|
833
778
|
x: focusTarget.x,
|
|
@@ -845,9 +790,7 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
845
790
|
targetY: 0,
|
|
846
791
|
targetZ: 0,
|
|
847
792
|
};
|
|
848
|
-
|
|
849
|
-
initialPosRef.current = newPos;
|
|
850
|
-
console.log('[useEffect] Starting animation to:', newPos);
|
|
793
|
+
console.log('[useEffect] Animating to:', newPos);
|
|
851
794
|
api.start({
|
|
852
795
|
camX: newPos.x,
|
|
853
796
|
camY: newPos.y,
|
|
@@ -855,109 +798,52 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
855
798
|
lookX: newPos.targetX,
|
|
856
799
|
lookY: newPos.targetY,
|
|
857
800
|
lookZ: newPos.targetZ,
|
|
858
|
-
onRest: () => {
|
|
859
|
-
console.log('[useEffect animation] onRest called');
|
|
860
|
-
isAnimatingRef.current = false;
|
|
861
|
-
},
|
|
862
801
|
});
|
|
863
|
-
|
|
802
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
803
|
+
}, [isFlat]); // Only animate when isFlat changes, not when focusTarget/citySize/etc change
|
|
864
804
|
// Update camera each frame
|
|
865
805
|
useFrame(() => {
|
|
866
806
|
frameCount.current++;
|
|
867
|
-
//
|
|
868
|
-
//
|
|
807
|
+
// On Frame 1: Set camera to initial 2D position and mark as ready
|
|
808
|
+
// Component always starts in 2D mode, so we just need to set the correct position once
|
|
869
809
|
if (frameCount.current === 1) {
|
|
870
|
-
// Ensure camera FOV is
|
|
810
|
+
// Ensure camera FOV is correct (defaults to 75 before prop applies)
|
|
871
811
|
const perspCam = camera;
|
|
872
812
|
if (perspCam.fov !== 50) {
|
|
873
813
|
console.log('[Frame 1] Correcting FOV from', perspCam.fov, 'to 50');
|
|
874
814
|
perspCam.fov = 50;
|
|
875
815
|
perspCam.updateProjectionMatrix();
|
|
876
816
|
}
|
|
877
|
-
// Calculate
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
:
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
x: 0,
|
|
902
|
-
y: maxBuildingHeight > 0 ? Math.max(citySize * 1.1, maxBuildingHeight * 2.5) : citySize * 1.1,
|
|
903
|
-
z: citySize * 1.3,
|
|
904
|
-
targetX: 0,
|
|
905
|
-
targetY: 0,
|
|
906
|
-
targetZ: 0,
|
|
907
|
-
};
|
|
908
|
-
console.log('[Frame 1] Calculated position:', freshPos);
|
|
909
|
-
initialPosRef.current = freshPos;
|
|
817
|
+
// Calculate initial 2D position with correct aspect ratio
|
|
818
|
+
const initialPos = getInitial2DPosition();
|
|
819
|
+
console.log('[Frame 1] Setting camera to initial 2D position:', initialPos);
|
|
820
|
+
camera.position.set(initialPos.x, initialPos.y, initialPos.z);
|
|
821
|
+
// Wait for controls to be ready, then set target and sync spring
|
|
822
|
+
if (controlsRef.current) {
|
|
823
|
+
controlsRef.current.target.set(initialPos.targetX, initialPos.targetY, initialPos.targetZ);
|
|
824
|
+
controlsRef.current.update();
|
|
825
|
+
// Sync spring to match camera position (use immediate to avoid animation)
|
|
826
|
+
api.start({
|
|
827
|
+
camX: initialPos.x,
|
|
828
|
+
camY: initialPos.y,
|
|
829
|
+
camZ: initialPos.z,
|
|
830
|
+
lookX: initialPos.targetX,
|
|
831
|
+
lookY: initialPos.targetY,
|
|
832
|
+
lookZ: initialPos.targetZ,
|
|
833
|
+
immediate: true,
|
|
834
|
+
});
|
|
835
|
+
hasAppliedInitial.current = true;
|
|
836
|
+
// Notify parent that camera is ready
|
|
837
|
+
if (!hasNotifiedReady.current && onCameraReady) {
|
|
838
|
+
hasNotifiedReady.current = true;
|
|
839
|
+
onCameraReady();
|
|
840
|
+
}
|
|
910
841
|
}
|
|
911
|
-
const pos = initialPosRef.current;
|
|
912
|
-
console.log('[Frame 1] Setting camera to:', pos);
|
|
913
|
-
camera.position.set(pos.x, pos.y, pos.z);
|
|
914
842
|
return;
|
|
915
843
|
}
|
|
916
|
-
//
|
|
917
|
-
if (
|
|
918
|
-
return;
|
|
919
|
-
if (!controlsRef.current)
|
|
920
|
-
return;
|
|
921
|
-
// Set initial target and sync spring (after OrbitControls is ready)
|
|
922
|
-
// Use frozen initialPosRef to match Frame 1 position
|
|
923
|
-
if (!hasAppliedInitial.current && initialPosRef.current) {
|
|
924
|
-
const pos = initialPosRef.current;
|
|
925
|
-
camera.position.set(pos.x, pos.y, pos.z);
|
|
926
|
-
controlsRef.current.target.set(pos.targetX, pos.targetY, pos.targetZ);
|
|
927
|
-
controlsRef.current.update();
|
|
928
|
-
// Sync spring to this position so future animations start from here
|
|
929
|
-
console.log('[Frame 3] Syncing spring to:', pos);
|
|
930
|
-
// Set flag to prevent onStart from triggering
|
|
931
|
-
isSyncingInitial.current = true;
|
|
932
|
-
// Stop any ongoing animations first
|
|
933
|
-
api.stop();
|
|
934
|
-
// Use api.start with immediate: true to set both current AND target values
|
|
935
|
-
// This ensures the spring won't try to animate back to any previous target
|
|
936
|
-
api.start({
|
|
937
|
-
camX: pos.x,
|
|
938
|
-
camY: pos.y,
|
|
939
|
-
camZ: pos.z,
|
|
940
|
-
lookX: pos.targetX,
|
|
941
|
-
lookY: pos.targetY,
|
|
942
|
-
lookZ: pos.targetZ,
|
|
943
|
-
immediate: true,
|
|
944
|
-
});
|
|
945
|
-
// Clear the syncing flag after a small delay to ensure spring is settled
|
|
946
|
-
setTimeout(() => {
|
|
947
|
-
isSyncingInitial.current = false;
|
|
948
|
-
console.log('[Frame 3] Sync complete - spring ready for animations');
|
|
949
|
-
}, 100);
|
|
950
|
-
// Ensure animation flag is off
|
|
951
|
-
isAnimatingRef.current = false;
|
|
952
|
-
console.log('[Frame 3] Spring values after sync:', { camY: camY.get() });
|
|
953
|
-
hasAppliedInitial.current = true;
|
|
954
|
-
// Notify parent that camera is ready (only once)
|
|
955
|
-
if (!hasNotifiedReady.current && onCameraReady) {
|
|
956
|
-
hasNotifiedReady.current = true;
|
|
957
|
-
onCameraReady();
|
|
958
|
-
}
|
|
844
|
+
// Wait for controls and initialization to complete
|
|
845
|
+
if (!controlsRef.current || !hasAppliedInitial.current)
|
|
959
846
|
return;
|
|
960
|
-
}
|
|
961
847
|
// Handle orbit animation (horizontal rotation along arc)
|
|
962
848
|
if (isOrbitingRef.current && orbitParamsRef.current) {
|
|
963
849
|
const { centerX, centerZ, distance, height } = orbitParamsRef.current;
|
|
@@ -1247,6 +1133,12 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1247
1133
|
};
|
|
1248
1134
|
}, [resetToInitial, moveTo, setTarget, rotateTo, rotateBy, tiltTo, tiltBy, getCurrentPosition, getCurrentTarget, getCurrentAngle, getCurrentTilt]);
|
|
1249
1135
|
return (_jsxs(_Fragment, { children: [_jsx(PerspectiveCamera, { makeDefault: true, fov: 50, near: 1, far: citySize * 10 }), _jsx(OrbitControls, { ref: controlsRef, enableDamping: true, dampingFactor: 0.05, minDistance: 10, maxDistance: citySize * 3, maxPolarAngle: Math.PI / 2.1 })] }));
|
|
1136
|
+
}, (prevProps, nextProps) => {
|
|
1137
|
+
// Custom comparison: only re-render if isFlat, citySize, or maxBuildingHeight changes
|
|
1138
|
+
// Skip re-render when only focusTarget changes (handled internally by useEffect on isFlat)
|
|
1139
|
+
return (prevProps.isFlat === nextProps.isFlat &&
|
|
1140
|
+
prevProps.citySize === nextProps.citySize &&
|
|
1141
|
+
prevProps.maxBuildingHeight === nextProps.maxBuildingHeight);
|
|
1250
1142
|
});
|
|
1251
1143
|
function InfoPanel({ building }) {
|
|
1252
1144
|
if (!building)
|
|
@@ -1466,10 +1358,6 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
1466
1358
|
return null;
|
|
1467
1359
|
return cityData.buildings.findIndex(b => b.path === selectedBuilding.path);
|
|
1468
1360
|
}, [selectedBuilding, cityData.buildings]);
|
|
1469
|
-
// Calculate spring duration for animation sync
|
|
1470
|
-
const tension = animationConfig.tension || 120;
|
|
1471
|
-
const friction = animationConfig.friction || 14;
|
|
1472
|
-
const springDuration = Math.sqrt(1 / (tension * 0.001)) * friction * 20;
|
|
1473
1361
|
return (_jsxs(_Fragment, { children: [_jsx(AnimatedCamera, { citySize: citySize, isFlat: growProgress === 0, focusTarget: focusTarget, maxBuildingHeight: maxBuildingHeight, onCameraReady: onCameraReady }), _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 => {
|
|
1474
1362
|
// Check if district matches focusDirectory
|
|
1475
1363
|
const isFocused = buildingFocusDirectory
|
package/package.json
CHANGED
|
@@ -1156,101 +1156,56 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1156
1156
|
const frameCount = useRef(0);
|
|
1157
1157
|
const hasNotifiedReady = useRef(false);
|
|
1158
1158
|
const prevIsFlatRef = useRef(isFlat); // Track previous isFlat to detect actual state changes
|
|
1159
|
-
const isSyncingInitial = useRef(false); // Flag to prevent onStart from triggering during Frame 3 sync
|
|
1160
1159
|
|
|
1161
|
-
//
|
|
1160
|
+
// Helper to calculate flat camera height with known FOV (50) and aspect ratio
|
|
1162
1161
|
// Formula: height = citySize / (2 * tan(fov/2) * min(1, aspect))
|
|
1163
1162
|
// Padding factor adds space around the city to match 2D component
|
|
1164
|
-
const calculateFlatCameraHeight = useCallback(() => {
|
|
1165
|
-
const
|
|
1166
|
-
const fovRad = (
|
|
1163
|
+
const calculateFlatCameraHeight = useCallback((aspect: number) => {
|
|
1164
|
+
const fov = 50; // Known FOV that will be set on PerspectiveCamera
|
|
1165
|
+
const fovRad = (fov * Math.PI) / 180;
|
|
1167
1166
|
const tanHalfFov = Math.tan(fovRad / 2);
|
|
1168
|
-
const aspect = perspCam.aspect || 1;
|
|
1169
1167
|
// Use min(1, aspect) to handle both landscape and portrait viewports
|
|
1170
1168
|
const effectiveAspect = Math.min(1, aspect);
|
|
1171
1169
|
const baseHeight = citySize / (2 * tanHalfFov * effectiveAspect);
|
|
1172
1170
|
// Add padding to match 2D component's default padding
|
|
1173
1171
|
const paddingFactor = 1.08;
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
fov: perspCam.fov,
|
|
1177
|
-
aspect,
|
|
1178
|
-
effectiveAspect,
|
|
1179
|
-
citySize,
|
|
1180
|
-
baseHeight,
|
|
1181
|
-
result,
|
|
1182
|
-
});
|
|
1183
|
-
return result;
|
|
1184
|
-
}, [camera, citySize]);
|
|
1185
|
-
|
|
1186
|
-
// Compute target camera position
|
|
1187
|
-
// When flat, always use top-down view (ignore focusTarget)
|
|
1188
|
-
// When grown, use focusTarget if available, otherwise angled overview
|
|
1189
|
-
const targetPos = useMemo(() => {
|
|
1190
|
-
// Flat state: always top-down, ignore any focus
|
|
1191
|
-
// Height calculated mathematically to match 2D view zoom level
|
|
1192
|
-
if (isFlat) {
|
|
1193
|
-
return {
|
|
1194
|
-
x: 0,
|
|
1195
|
-
y: calculateFlatCameraHeight(),
|
|
1196
|
-
z: 0.001, // Near-zero for top-down (tiny offset to avoid gimbal lock)
|
|
1197
|
-
targetX: 0,
|
|
1198
|
-
targetY: 0,
|
|
1199
|
-
targetZ: 0,
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1172
|
+
return baseHeight * paddingFactor;
|
|
1173
|
+
}, [citySize]);
|
|
1202
1174
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
y: height,
|
|
1210
|
-
z: focusTarget.z + distance,
|
|
1211
|
-
targetX: focusTarget.x,
|
|
1212
|
-
targetY: 0,
|
|
1213
|
-
targetZ: focusTarget.z,
|
|
1214
|
-
};
|
|
1215
|
-
}
|
|
1175
|
+
// Calculate initial 2D position (component always starts in 2D mode)
|
|
1176
|
+
// We need aspect ratio from the camera, but we'll use a default until Frame 1
|
|
1177
|
+
const getInitial2DPosition = useCallback(() => {
|
|
1178
|
+
const perspCam = camera as THREE.PerspectiveCamera;
|
|
1179
|
+
const aspect = perspCam.aspect || 1;
|
|
1180
|
+
const height = calculateFlatCameraHeight(aspect);
|
|
1216
1181
|
|
|
1217
|
-
// Grown state without focus: angled overview
|
|
1218
|
-
const baseHeight = citySize * 1.1;
|
|
1219
|
-
const buildingAwareHeight = maxBuildingHeight > 0
|
|
1220
|
-
? Math.max(baseHeight, maxBuildingHeight * 2.5)
|
|
1221
|
-
: baseHeight;
|
|
1222
1182
|
return {
|
|
1223
1183
|
x: 0,
|
|
1224
|
-
y:
|
|
1225
|
-
z:
|
|
1184
|
+
y: height,
|
|
1185
|
+
z: 0.001, // Near-zero for top-down (tiny offset to avoid gimbal lock)
|
|
1226
1186
|
targetX: 0,
|
|
1227
1187
|
targetY: 0,
|
|
1228
1188
|
targetZ: 0,
|
|
1229
1189
|
};
|
|
1230
|
-
}, [
|
|
1231
|
-
|
|
1232
|
-
// Freeze initial camera position on Frame 1 (not during render)
|
|
1233
|
-
// This ensures we capture the correct calculation after canvas is initialized
|
|
1234
|
-
const initialPosRef = useRef<typeof targetPos | null>(null);
|
|
1190
|
+
}, [camera, calculateFlatCameraHeight]);
|
|
1235
1191
|
|
|
1236
1192
|
// Spring animation for camera movement
|
|
1237
|
-
// Initialize
|
|
1193
|
+
// Initialize with correct 2D position from the start
|
|
1238
1194
|
const [{ camX, camY, camZ, lookX, lookY, lookZ }, api] = useSpring(() => {
|
|
1239
|
-
|
|
1195
|
+
// Calculate initial position with default aspect ratio
|
|
1196
|
+
// This will be corrected in Frame 1 if aspect is different
|
|
1197
|
+
const initialHeight = calculateFlatCameraHeight(1);
|
|
1198
|
+
|
|
1199
|
+
console.log('[Spring init] Initializing with 2D position, height:', initialHeight);
|
|
1240
1200
|
return {
|
|
1241
1201
|
camX: 0,
|
|
1242
|
-
camY:
|
|
1243
|
-
camZ: 0,
|
|
1202
|
+
camY: initialHeight,
|
|
1203
|
+
camZ: 0.001,
|
|
1244
1204
|
lookX: 0,
|
|
1245
1205
|
lookY: 0,
|
|
1246
1206
|
lookZ: 0,
|
|
1247
1207
|
config: { tension: 60, friction: 20 },
|
|
1248
1208
|
onStart: () => {
|
|
1249
|
-
// Block animations during initial sync in Frame 3
|
|
1250
|
-
if (isSyncingInitial.current) {
|
|
1251
|
-
console.log('[Spring onStart] Blocked - syncing initial position');
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
1209
|
// Only allow animations after initial setup is complete
|
|
1255
1210
|
if (hasAppliedInitial.current) {
|
|
1256
1211
|
console.log('[Spring onStart] Animation starting - camY:', camY.get());
|
|
@@ -1308,42 +1263,31 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1308
1263
|
azimuthAngle: number; // horizontal angle to maintain
|
|
1309
1264
|
} | null>(null);
|
|
1310
1265
|
|
|
1311
|
-
// When isFlat changes
|
|
1312
|
-
//
|
|
1313
|
-
// from aspect ratio changes or other recalculations
|
|
1266
|
+
// When isFlat changes from true to false, animate to 3D view
|
|
1267
|
+
// Component always starts in 2D, so we only animate the 2D→3D transition
|
|
1314
1268
|
useEffect(() => {
|
|
1315
|
-
console.log('[useEffect]
|
|
1316
|
-
console.log('[useEffect] Dependency check - focusTarget:', focusTarget, 'citySize:', citySize, 'maxBuildingHeight:', maxBuildingHeight);
|
|
1269
|
+
console.log('[useEffect] isFlat:', isFlat, 'prevIsFlat:', prevIsFlatRef.current, 'hasAppliedInitial:', hasAppliedInitial.current);
|
|
1317
1270
|
|
|
1318
1271
|
// Skip until camera is initialized
|
|
1319
|
-
if (!hasAppliedInitial.current
|
|
1272
|
+
if (!hasAppliedInitial.current) {
|
|
1320
1273
|
console.log('[useEffect] Skipping - not initialized yet');
|
|
1321
1274
|
return;
|
|
1322
1275
|
}
|
|
1323
1276
|
|
|
1324
|
-
// Only animate if isFlat
|
|
1277
|
+
// Only animate if isFlat changed from true to false (2D → 3D transition)
|
|
1325
1278
|
const isFlatChanged = prevIsFlatRef.current !== isFlat;
|
|
1326
1279
|
|
|
1327
1280
|
if (!isFlatChanged) {
|
|
1328
|
-
|
|
1329
|
-
// Use initialPosRef to prevent updates from targetPos recalculations
|
|
1330
|
-
console.log('[useEffect] No isFlat change - skipping (prevIsFlat:', prevIsFlatRef.current, 'isFlat:', isFlat, ')');
|
|
1281
|
+
console.log('[useEffect] No isFlat change - skipping');
|
|
1331
1282
|
return;
|
|
1332
1283
|
}
|
|
1333
1284
|
|
|
1334
1285
|
console.log('[useEffect] isFlat changed from', prevIsFlatRef.current, 'to', isFlat, '- animating transition');
|
|
1335
1286
|
prevIsFlatRef.current = isFlat;
|
|
1336
1287
|
|
|
1337
|
-
//
|
|
1288
|
+
// Calculate target position for 3D view
|
|
1338
1289
|
const newPos = isFlat
|
|
1339
|
-
?
|
|
1340
|
-
x: 0,
|
|
1341
|
-
y: calculateFlatCameraHeight(),
|
|
1342
|
-
z: 0.001,
|
|
1343
|
-
targetX: 0,
|
|
1344
|
-
targetY: 0,
|
|
1345
|
-
targetZ: 0,
|
|
1346
|
-
}
|
|
1290
|
+
? getInitial2DPosition() // Going back to 2D
|
|
1347
1291
|
: focusTarget
|
|
1348
1292
|
? {
|
|
1349
1293
|
x: focusTarget.x,
|
|
@@ -1362,10 +1306,7 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1362
1306
|
targetZ: 0,
|
|
1363
1307
|
};
|
|
1364
1308
|
|
|
1365
|
-
|
|
1366
|
-
initialPosRef.current = newPos;
|
|
1367
|
-
|
|
1368
|
-
console.log('[useEffect] Starting animation to:', newPos);
|
|
1309
|
+
console.log('[useEffect] Animating to:', newPos);
|
|
1369
1310
|
api.start({
|
|
1370
1311
|
camX: newPos.x,
|
|
1371
1312
|
camY: newPos.y,
|
|
@@ -1373,21 +1314,18 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1373
1314
|
lookX: newPos.targetX,
|
|
1374
1315
|
lookY: newPos.targetY,
|
|
1375
1316
|
lookZ: newPos.targetZ,
|
|
1376
|
-
onRest: () => {
|
|
1377
|
-
console.log('[useEffect animation] onRest called');
|
|
1378
|
-
isAnimatingRef.current = false;
|
|
1379
|
-
},
|
|
1380
1317
|
});
|
|
1381
|
-
|
|
1318
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1319
|
+
}, [isFlat]); // Only animate when isFlat changes, not when focusTarget/citySize/etc change
|
|
1382
1320
|
|
|
1383
1321
|
// Update camera each frame
|
|
1384
1322
|
useFrame(() => {
|
|
1385
1323
|
frameCount.current++;
|
|
1386
1324
|
|
|
1387
|
-
//
|
|
1388
|
-
//
|
|
1325
|
+
// On Frame 1: Set camera to initial 2D position and mark as ready
|
|
1326
|
+
// Component always starts in 2D mode, so we just need to set the correct position once
|
|
1389
1327
|
if (frameCount.current === 1) {
|
|
1390
|
-
// Ensure camera FOV is
|
|
1328
|
+
// Ensure camera FOV is correct (defaults to 75 before prop applies)
|
|
1391
1329
|
const perspCam = camera as THREE.PerspectiveCamera;
|
|
1392
1330
|
if (perspCam.fov !== 50) {
|
|
1393
1331
|
console.log('[Frame 1] Correcting FOV from', perspCam.fov, 'to 50');
|
|
@@ -1395,99 +1333,41 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1395
1333
|
perspCam.updateProjectionMatrix();
|
|
1396
1334
|
}
|
|
1397
1335
|
|
|
1398
|
-
// Calculate
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
:
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
targetZ: 0,
|
|
1428
|
-
};
|
|
1429
|
-
console.log('[Frame 1] Calculated position:', freshPos);
|
|
1430
|
-
initialPosRef.current = freshPos;
|
|
1336
|
+
// Calculate initial 2D position with correct aspect ratio
|
|
1337
|
+
const initialPos = getInitial2DPosition();
|
|
1338
|
+
console.log('[Frame 1] Setting camera to initial 2D position:', initialPos);
|
|
1339
|
+
|
|
1340
|
+
camera.position.set(initialPos.x, initialPos.y, initialPos.z);
|
|
1341
|
+
|
|
1342
|
+
// Wait for controls to be ready, then set target and sync spring
|
|
1343
|
+
if (controlsRef.current) {
|
|
1344
|
+
controlsRef.current.target.set(initialPos.targetX, initialPos.targetY, initialPos.targetZ);
|
|
1345
|
+
controlsRef.current.update();
|
|
1346
|
+
|
|
1347
|
+
// Sync spring to match camera position (use immediate to avoid animation)
|
|
1348
|
+
api.start({
|
|
1349
|
+
camX: initialPos.x,
|
|
1350
|
+
camY: initialPos.y,
|
|
1351
|
+
camZ: initialPos.z,
|
|
1352
|
+
lookX: initialPos.targetX,
|
|
1353
|
+
lookY: initialPos.targetY,
|
|
1354
|
+
lookZ: initialPos.targetZ,
|
|
1355
|
+
immediate: true,
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
hasAppliedInitial.current = true;
|
|
1359
|
+
|
|
1360
|
+
// Notify parent that camera is ready
|
|
1361
|
+
if (!hasNotifiedReady.current && onCameraReady) {
|
|
1362
|
+
hasNotifiedReady.current = true;
|
|
1363
|
+
onCameraReady();
|
|
1364
|
+
}
|
|
1431
1365
|
}
|
|
1432
|
-
const pos = initialPosRef.current;
|
|
1433
|
-
console.log('[Frame 1] Setting camera to:', pos);
|
|
1434
|
-
camera.position.set(pos.x, pos.y, pos.z);
|
|
1435
1366
|
return;
|
|
1436
1367
|
}
|
|
1437
1368
|
|
|
1438
|
-
//
|
|
1439
|
-
if (
|
|
1440
|
-
if (!controlsRef.current) return;
|
|
1441
|
-
|
|
1442
|
-
// Set initial target and sync spring (after OrbitControls is ready)
|
|
1443
|
-
// Use frozen initialPosRef to match Frame 1 position
|
|
1444
|
-
if (!hasAppliedInitial.current && initialPosRef.current) {
|
|
1445
|
-
const pos = initialPosRef.current;
|
|
1446
|
-
camera.position.set(pos.x, pos.y, pos.z);
|
|
1447
|
-
controlsRef.current.target.set(pos.targetX, pos.targetY, pos.targetZ);
|
|
1448
|
-
controlsRef.current.update();
|
|
1449
|
-
|
|
1450
|
-
// Sync spring to this position so future animations start from here
|
|
1451
|
-
console.log('[Frame 3] Syncing spring to:', pos);
|
|
1452
|
-
|
|
1453
|
-
// Set flag to prevent onStart from triggering
|
|
1454
|
-
isSyncingInitial.current = true;
|
|
1455
|
-
|
|
1456
|
-
// Stop any ongoing animations first
|
|
1457
|
-
api.stop();
|
|
1458
|
-
|
|
1459
|
-
// Use api.start with immediate: true to set both current AND target values
|
|
1460
|
-
// This ensures the spring won't try to animate back to any previous target
|
|
1461
|
-
api.start({
|
|
1462
|
-
camX: pos.x,
|
|
1463
|
-
camY: pos.y,
|
|
1464
|
-
camZ: pos.z,
|
|
1465
|
-
lookX: pos.targetX,
|
|
1466
|
-
lookY: pos.targetY,
|
|
1467
|
-
lookZ: pos.targetZ,
|
|
1468
|
-
immediate: true,
|
|
1469
|
-
});
|
|
1470
|
-
|
|
1471
|
-
// Clear the syncing flag after a small delay to ensure spring is settled
|
|
1472
|
-
setTimeout(() => {
|
|
1473
|
-
isSyncingInitial.current = false;
|
|
1474
|
-
console.log('[Frame 3] Sync complete - spring ready for animations');
|
|
1475
|
-
}, 100);
|
|
1476
|
-
|
|
1477
|
-
// Ensure animation flag is off
|
|
1478
|
-
isAnimatingRef.current = false;
|
|
1479
|
-
|
|
1480
|
-
console.log('[Frame 3] Spring values after sync:', { camY: camY.get() });
|
|
1481
|
-
|
|
1482
|
-
hasAppliedInitial.current = true;
|
|
1483
|
-
|
|
1484
|
-
// Notify parent that camera is ready (only once)
|
|
1485
|
-
if (!hasNotifiedReady.current && onCameraReady) {
|
|
1486
|
-
hasNotifiedReady.current = true;
|
|
1487
|
-
onCameraReady();
|
|
1488
|
-
}
|
|
1489
|
-
return;
|
|
1490
|
-
}
|
|
1369
|
+
// Wait for controls and initialization to complete
|
|
1370
|
+
if (!controlsRef.current || !hasAppliedInitial.current) return;
|
|
1491
1371
|
|
|
1492
1372
|
// Handle orbit animation (horizontal rotation along arc)
|
|
1493
1373
|
if (isOrbitingRef.current && orbitParamsRef.current) {
|
|
@@ -1846,6 +1726,14 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1846
1726
|
/>
|
|
1847
1727
|
</>
|
|
1848
1728
|
);
|
|
1729
|
+
}, (prevProps, nextProps) => {
|
|
1730
|
+
// Custom comparison: only re-render if isFlat, citySize, or maxBuildingHeight changes
|
|
1731
|
+
// Skip re-render when only focusTarget changes (handled internally by useEffect on isFlat)
|
|
1732
|
+
return (
|
|
1733
|
+
prevProps.isFlat === nextProps.isFlat &&
|
|
1734
|
+
prevProps.citySize === nextProps.citySize &&
|
|
1735
|
+
prevProps.maxBuildingHeight === nextProps.maxBuildingHeight
|
|
1736
|
+
);
|
|
1849
1737
|
});
|
|
1850
1738
|
|
|
1851
1739
|
// Info panel overlay
|
|
@@ -2199,11 +2087,6 @@ function CityScene({
|
|
|
2199
2087
|
return cityData.buildings.findIndex(b => b.path === selectedBuilding.path);
|
|
2200
2088
|
}, [selectedBuilding, cityData.buildings]);
|
|
2201
2089
|
|
|
2202
|
-
// Calculate spring duration for animation sync
|
|
2203
|
-
const tension = animationConfig.tension || 120;
|
|
2204
|
-
const friction = animationConfig.friction || 14;
|
|
2205
|
-
const springDuration = Math.sqrt(1 / (tension * 0.001)) * friction * 20;
|
|
2206
|
-
|
|
2207
2090
|
return (
|
|
2208
2091
|
<>
|
|
2209
2092
|
<AnimatedCamera
|