@principal-ai/file-city-react 0.5.12 → 0.5.14

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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"}
@@ -287,15 +287,16 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
287
287
  return;
288
288
  buildings.forEach((building, index) => {
289
289
  let shouldCollapse = false;
290
- // Priority 1: focusDirectory - collapse buildings outside
291
- if (focusDirectory) {
292
- const isInFocus = isPathInDirectory(building.path, focusDirectory);
293
- shouldCollapse = !isInFocus;
294
- }
295
- // Priority 2: highlightLayers with collapse isolation mode
296
- else if (hasActiveHighlightLayers && isolationMode === 'collapse') {
297
- const highlight = getHighlightForPath(building.path, highlightLayers);
298
- shouldCollapse = highlight === null;
290
+ const isInFocusDirectory = focusDirectory
291
+ ? isPathInDirectory(building.path, focusDirectory)
292
+ : true; // No focusDirectory means all are "in focus"
293
+ const isHighlighted = hasActiveHighlightLayers
294
+ ? getHighlightForPath(building.path, highlightLayers) !== null
295
+ : true; // No highlights means all are "highlighted"
296
+ // Collapse if outside BOTH focusDirectory AND highlightLayers
297
+ // (only when collapse mode is active)
298
+ if (focusDirectory || (hasActiveHighlightLayers && isolationMode === 'collapse')) {
299
+ shouldCollapse = !isInFocusDirectory && !isHighlighted;
299
300
  }
300
301
  targetMultipliersRef.current[index] = shouldCollapse ? 0.05 : 1;
301
302
  });
@@ -569,12 +570,12 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }) {
569
570
  const { camera } = useThree();
570
571
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
571
572
  const controlsRef = useRef(null);
572
- // Start false - only block rotation during active camera animations
573
573
  const isAnimatingRef = useRef(false);
574
- // Animated camera position and target
574
+ const hasAppliedInitial = useRef(false);
575
+ const frameCount = useRef(0);
576
+ // Compute target camera position
575
577
  const targetPos = useMemo(() => {
576
578
  if (focusTarget) {
577
- // Position camera to look at focus target
578
579
  const distance = Math.max(focusTarget.size * 2, 50);
579
580
  const height = Math.max(focusTarget.size * 1.5, 40);
580
581
  return {
@@ -586,9 +587,9 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }) {
586
587
  targetZ: focusTarget.z,
587
588
  };
588
589
  }
589
- // Default: overview of entire city
590
- const targetHeight = isFlat ? citySize * 1.5 : citySize * 1.1;
591
- const targetZ = isFlat ? 0 : citySize * 1.3;
590
+ // Default overview
591
+ const targetHeight = citySize * 1.1;
592
+ const targetZ = citySize * 1.3;
592
593
  return {
593
594
  x: 0,
594
595
  y: targetHeight,
@@ -597,19 +598,9 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }) {
597
598
  targetY: 0,
598
599
  targetZ: 0,
599
600
  };
600
- }, [focusTarget, isFlat, citySize]);
601
- // Set initial camera position on mount
602
- useEffect(() => {
603
- camera.position.set(targetPos.x, targetPos.y, targetPos.z);
604
- if (controlsRef.current) {
605
- controlsRef.current.target.set(targetPos.targetX, targetPos.targetY, targetPos.targetZ);
606
- controlsRef.current.update();
607
- }
608
- // Only run on mount
609
- // eslint-disable-next-line react-hooks/exhaustive-deps
610
- }, []);
611
- // Spring animation for camera movement (only for subsequent changes)
612
- const { camX, camY, camZ, lookX, lookY, lookZ } = useSpring({
601
+ }, [focusTarget, citySize]);
602
+ // Spring animation for camera movement
603
+ const [{ camX, camY, camZ, lookX, lookY, lookZ }, api] = useSpring(() => ({
613
604
  camX: targetPos.x,
614
605
  camY: targetPos.y,
615
606
  camZ: targetPos.z,
@@ -623,31 +614,68 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }) {
623
614
  onRest: () => {
624
615
  isAnimatingRef.current = false;
625
616
  },
626
- });
627
- // Update camera each frame only while spring is animating
628
- // Once animation settles, let OrbitControls handle user interaction
617
+ }));
618
+ // When targetPos changes after initial, animate to new position
619
+ useEffect(() => {
620
+ // Skip the first render - we handle that directly in useFrame
621
+ if (!hasAppliedInitial.current)
622
+ return;
623
+ api.start({
624
+ camX: targetPos.x,
625
+ camY: targetPos.y,
626
+ camZ: targetPos.z,
627
+ lookX: targetPos.targetX,
628
+ lookY: targetPos.targetY,
629
+ lookZ: targetPos.targetZ,
630
+ onRest: () => {
631
+ isAnimatingRef.current = false;
632
+ },
633
+ });
634
+ }, [targetPos, api]);
635
+ // Update camera each frame
629
636
  useFrame(() => {
630
- if (!controlsRef.current || !isAnimatingRef.current)
637
+ frameCount.current++;
638
+ // Skip first 2 frames to ensure OrbitControls is fully initialized
639
+ if (frameCount.current < 3)
631
640
  return;
632
- camera.position.set(camX.get(), camY.get(), camZ.get());
633
- controlsRef.current.target.set(lookX.get(), lookY.get(), lookZ.get());
634
- controlsRef.current.update();
635
- });
636
- const resetToInitial = useCallback(() => {
637
- const targetHeight = isFlat ? citySize * 1.5 : citySize * 1.1;
638
- const targetZ = isFlat ? 0 : citySize * 1.3;
639
- camera.position.set(0, targetHeight, targetZ);
640
- camera.lookAt(0, 0, 0);
641
- if (controlsRef.current) {
642
- controlsRef.current.target.set(0, 0, 0);
641
+ if (!controlsRef.current)
642
+ return;
643
+ // Set initial position: apply camera position directly (no spring animation)
644
+ if (!hasAppliedInitial.current) {
645
+ camera.position.set(targetPos.x, targetPos.y, targetPos.z);
646
+ controlsRef.current.target.set(targetPos.targetX, targetPos.targetY, targetPos.targetZ);
643
647
  controlsRef.current.update();
648
+ // Sync spring to this position so future animations start from here
649
+ api.set({
650
+ camX: targetPos.x,
651
+ camY: targetPos.y,
652
+ camZ: targetPos.z,
653
+ lookX: targetPos.targetX,
654
+ lookY: targetPos.targetY,
655
+ lookZ: targetPos.targetZ,
656
+ });
657
+ hasAppliedInitial.current = true;
658
+ return;
644
659
  }
645
- }, [isFlat, citySize, camera]);
646
- useEffect(() => {
647
- if (!focusTarget) {
648
- resetToInitial();
660
+ // Subsequent frames: only update during animations
661
+ if (isAnimatingRef.current) {
662
+ camera.position.set(camX.get(), camY.get(), camZ.get());
663
+ controlsRef.current.target.set(lookX.get(), lookY.get(), lookZ.get());
664
+ controlsRef.current.update();
649
665
  }
650
- }, [resetToInitial, focusTarget]);
666
+ });
667
+ const resetToInitial = useCallback(() => {
668
+ const targetHeight = citySize * 1.1;
669
+ const targetZ = citySize * 1.3;
670
+ api.start({
671
+ camX: 0,
672
+ camY: targetHeight,
673
+ camZ: targetZ,
674
+ lookX: 0,
675
+ lookY: 0,
676
+ lookZ: 0,
677
+ });
678
+ }, [citySize, api]);
651
679
  useEffect(() => {
652
680
  cameraResetFn = resetToInitial;
653
681
  return () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/file-city-react",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "type": "module",
5
5
  "description": "React components for File City visualization",
6
6
  "main": "dist/index.js",
@@ -491,15 +491,18 @@ function InstancedBuildings({
491
491
  buildings.forEach((building, index) => {
492
492
  let shouldCollapse = false;
493
493
 
494
- // Priority 1: focusDirectory - collapse buildings outside
495
- if (focusDirectory) {
496
- const isInFocus = isPathInDirectory(building.path, focusDirectory);
497
- shouldCollapse = !isInFocus;
498
- }
499
- // Priority 2: highlightLayers with collapse isolation mode
500
- else if (hasActiveHighlightLayers && isolationMode === 'collapse') {
501
- const highlight = getHighlightForPath(building.path, highlightLayers);
502
- shouldCollapse = highlight === null;
494
+ const isInFocusDirectory = focusDirectory
495
+ ? isPathInDirectory(building.path, focusDirectory)
496
+ : true; // No focusDirectory means all are "in focus"
497
+
498
+ const isHighlighted = hasActiveHighlightLayers
499
+ ? getHighlightForPath(building.path, highlightLayers) !== null
500
+ : true; // No highlights means all are "highlighted"
501
+
502
+ // Collapse if outside BOTH focusDirectory AND highlightLayers
503
+ // (only when collapse mode is active)
504
+ if (focusDirectory || (hasActiveHighlightLayers && isolationMode === 'collapse')) {
505
+ shouldCollapse = !isInFocusDirectory && !isHighlighted;
503
506
  }
504
507
 
505
508
  targetMultipliersRef.current![index] = shouldCollapse ? 0.05 : 1;
@@ -996,13 +999,13 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }: AnimatedCameraProps)
996
999
  const { camera } = useThree();
997
1000
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
998
1001
  const controlsRef = useRef<any>(null);
999
- // Start false - only block rotation during active camera animations
1000
1002
  const isAnimatingRef = useRef(false);
1003
+ const hasAppliedInitial = useRef(false);
1004
+ const frameCount = useRef(0);
1001
1005
 
1002
- // Animated camera position and target
1006
+ // Compute target camera position
1003
1007
  const targetPos = useMemo(() => {
1004
1008
  if (focusTarget) {
1005
- // Position camera to look at focus target
1006
1009
  const distance = Math.max(focusTarget.size * 2, 50);
1007
1010
  const height = Math.max(focusTarget.size * 1.5, 40);
1008
1011
  return {
@@ -1014,9 +1017,9 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }: AnimatedCameraProps)
1014
1017
  targetZ: focusTarget.z,
1015
1018
  };
1016
1019
  }
1017
- // Default: overview of entire city
1018
- const targetHeight = isFlat ? citySize * 1.5 : citySize * 1.1;
1019
- const targetZ = isFlat ? 0 : citySize * 1.3;
1020
+ // Default overview
1021
+ const targetHeight = citySize * 1.1;
1022
+ const targetZ = citySize * 1.3;
1020
1023
  return {
1021
1024
  x: 0,
1022
1025
  y: targetHeight,
@@ -1025,21 +1028,10 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }: AnimatedCameraProps)
1025
1028
  targetY: 0,
1026
1029
  targetZ: 0,
1027
1030
  };
1028
- }, [focusTarget, isFlat, citySize]);
1029
-
1030
- // Set initial camera position on mount
1031
- useEffect(() => {
1032
- camera.position.set(targetPos.x, targetPos.y, targetPos.z);
1033
- if (controlsRef.current) {
1034
- controlsRef.current.target.set(targetPos.targetX, targetPos.targetY, targetPos.targetZ);
1035
- controlsRef.current.update();
1036
- }
1037
- // Only run on mount
1038
- // eslint-disable-next-line react-hooks/exhaustive-deps
1039
- }, []);
1031
+ }, [focusTarget, citySize]);
1040
1032
 
1041
- // Spring animation for camera movement (only for subsequent changes)
1042
- const { camX, camY, camZ, lookX, lookY, lookZ } = useSpring({
1033
+ // Spring animation for camera movement
1034
+ const [{ camX, camY, camZ, lookX, lookY, lookZ }, api] = useSpring(() => ({
1043
1035
  camX: targetPos.x,
1044
1036
  camY: targetPos.y,
1045
1037
  camZ: targetPos.z,
@@ -1053,36 +1045,75 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }: AnimatedCameraProps)
1053
1045
  onRest: () => {
1054
1046
  isAnimatingRef.current = false;
1055
1047
  },
1056
- });
1048
+ }));
1049
+
1050
+ // When targetPos changes after initial, animate to new position
1051
+ useEffect(() => {
1052
+ // Skip the first render - we handle that directly in useFrame
1053
+ if (!hasAppliedInitial.current) return;
1054
+
1055
+ api.start({
1056
+ camX: targetPos.x,
1057
+ camY: targetPos.y,
1058
+ camZ: targetPos.z,
1059
+ lookX: targetPos.targetX,
1060
+ lookY: targetPos.targetY,
1061
+ lookZ: targetPos.targetZ,
1062
+ onRest: () => {
1063
+ isAnimatingRef.current = false;
1064
+ },
1065
+ });
1066
+ }, [targetPos, api]);
1057
1067
 
1058
- // Update camera each frame only while spring is animating
1059
- // Once animation settles, let OrbitControls handle user interaction
1068
+ // Update camera each frame
1060
1069
  useFrame(() => {
1061
- if (!controlsRef.current || !isAnimatingRef.current) return;
1070
+ frameCount.current++;
1062
1071
 
1063
- camera.position.set(camX.get(), camY.get(), camZ.get());
1064
- controlsRef.current.target.set(lookX.get(), lookY.get(), lookZ.get());
1065
- controlsRef.current.update();
1066
- });
1072
+ // Skip first 2 frames to ensure OrbitControls is fully initialized
1073
+ if (frameCount.current < 3) return;
1074
+ if (!controlsRef.current) return;
1067
1075
 
1068
- const resetToInitial = useCallback(() => {
1069
- const targetHeight = isFlat ? citySize * 1.5 : citySize * 1.1;
1070
- const targetZ = isFlat ? 0 : citySize * 1.3;
1076
+ // Set initial position: apply camera position directly (no spring animation)
1077
+ if (!hasAppliedInitial.current) {
1078
+ camera.position.set(targetPos.x, targetPos.y, targetPos.z);
1079
+ controlsRef.current.target.set(targetPos.targetX, targetPos.targetY, targetPos.targetZ);
1080
+ controlsRef.current.update();
1071
1081
 
1072
- camera.position.set(0, targetHeight, targetZ);
1073
- camera.lookAt(0, 0, 0);
1082
+ // Sync spring to this position so future animations start from here
1083
+ api.set({
1084
+ camX: targetPos.x,
1085
+ camY: targetPos.y,
1086
+ camZ: targetPos.z,
1087
+ lookX: targetPos.targetX,
1088
+ lookY: targetPos.targetY,
1089
+ lookZ: targetPos.targetZ,
1090
+ });
1091
+
1092
+ hasAppliedInitial.current = true;
1093
+ return;
1094
+ }
1074
1095
 
1075
- if (controlsRef.current) {
1076
- controlsRef.current.target.set(0, 0, 0);
1096
+ // Subsequent frames: only update during animations
1097
+ if (isAnimatingRef.current) {
1098
+ camera.position.set(camX.get(), camY.get(), camZ.get());
1099
+ controlsRef.current.target.set(lookX.get(), lookY.get(), lookZ.get());
1077
1100
  controlsRef.current.update();
1078
1101
  }
1079
- }, [isFlat, citySize, camera]);
1102
+ });
1080
1103
 
1081
- useEffect(() => {
1082
- if (!focusTarget) {
1083
- resetToInitial();
1084
- }
1085
- }, [resetToInitial, focusTarget]);
1104
+ const resetToInitial = useCallback(() => {
1105
+ const targetHeight = citySize * 1.1;
1106
+ const targetZ = citySize * 1.3;
1107
+
1108
+ api.start({
1109
+ camX: 0,
1110
+ camY: targetHeight,
1111
+ camZ: targetZ,
1112
+ lookX: 0,
1113
+ lookY: 0,
1114
+ lookZ: 0,
1115
+ });
1116
+ }, [citySize, api]);
1086
1117
 
1087
1118
  useEffect(() => {
1088
1119
  cameraResetFn = resetToInitial;