@principal-ai/file-city-react 0.5.13 → 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
- const prevTargetRef = useRef(null);
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,11 +598,9 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }) {
597
598
  targetY: 0,
598
599
  targetZ: 0,
599
600
  };
600
- }, [focusTarget, isFlat, citySize]);
601
- // Create a stable key to detect when target actually changes
602
- const targetKey = `${targetPos.x},${targetPos.y},${targetPos.z},${targetPos.targetX},${targetPos.targetZ}`;
601
+ }, [focusTarget, citySize]);
603
602
  // Spring animation for camera movement
604
- const { camX, camY, camZ, lookX, lookY, lookZ } = useSpring({
603
+ const [{ camX, camY, camZ, lookX, lookY, lookZ }, api] = useSpring(() => ({
605
604
  camX: targetPos.x,
606
605
  camY: targetPos.y,
607
606
  camZ: targetPos.z,
@@ -609,44 +608,74 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }) {
609
608
  lookY: targetPos.targetY,
610
609
  lookZ: targetPos.targetZ,
611
610
  config: { tension: 60, friction: 20 },
612
- immediate: prevTargetRef.current === null, // Skip animation on first render
613
611
  onStart: () => {
614
612
  isAnimatingRef.current = true;
615
613
  },
616
614
  onRest: () => {
617
615
  isAnimatingRef.current = false;
618
616
  },
619
- });
620
- // Track target changes
617
+ }));
618
+ // When targetPos changes after initial, animate to new position
621
619
  useEffect(() => {
622
- prevTargetRef.current = targetKey;
623
- }, [targetKey]);
624
- // Update camera each frame while animating or on first frame
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
625
636
  useFrame(() => {
637
+ frameCount.current++;
638
+ // Skip first 2 frames to ensure OrbitControls is fully initialized
639
+ if (frameCount.current < 3)
640
+ return;
626
641
  if (!controlsRef.current)
627
642
  return;
628
- // On first frame or while animating, update camera from spring values
629
- if (prevTargetRef.current === null || isAnimatingRef.current) {
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);
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;
659
+ }
660
+ // Subsequent frames: only update during animations
661
+ if (isAnimatingRef.current) {
630
662
  camera.position.set(camX.get(), camY.get(), camZ.get());
631
663
  controlsRef.current.target.set(lookX.get(), lookY.get(), lookZ.get());
632
664
  controlsRef.current.update();
633
665
  }
634
666
  });
635
667
  const resetToInitial = useCallback(() => {
636
- const targetHeight = isFlat ? citySize * 1.5 : citySize * 1.1;
637
- const targetZ = isFlat ? 0 : citySize * 1.3;
638
- camera.position.set(0, targetHeight, targetZ);
639
- camera.lookAt(0, 0, 0);
640
- if (controlsRef.current) {
641
- controlsRef.current.target.set(0, 0, 0);
642
- controlsRef.current.update();
643
- }
644
- }, [isFlat, citySize, camera]);
645
- useEffect(() => {
646
- if (!focusTarget) {
647
- resetToInitial();
648
- }
649
- }, [resetToInitial, focusTarget]);
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]);
650
679
  useEffect(() => {
651
680
  cameraResetFn = resetToInitial;
652
681
  return () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/file-city-react",
3
- "version": "0.5.13",
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
- const prevTargetRef = useRef<string | null>(null);
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,13 +1028,10 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }: AnimatedCameraProps)
1025
1028
  targetY: 0,
1026
1029
  targetZ: 0,
1027
1030
  };
1028
- }, [focusTarget, isFlat, citySize]);
1029
-
1030
- // Create a stable key to detect when target actually changes
1031
- const targetKey = `${targetPos.x},${targetPos.y},${targetPos.z},${targetPos.targetX},${targetPos.targetZ}`;
1031
+ }, [focusTarget, citySize]);
1032
1032
 
1033
1033
  // Spring animation for camera movement
1034
- const { camX, camY, camZ, lookX, lookY, lookZ } = useSpring({
1034
+ const [{ camX, camY, camZ, lookX, lookY, lookZ }, api] = useSpring(() => ({
1035
1035
  camX: targetPos.x,
1036
1036
  camY: targetPos.y,
1037
1037
  camZ: targetPos.z,
@@ -1039,26 +1039,62 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }: AnimatedCameraProps)
1039
1039
  lookY: targetPos.targetY,
1040
1040
  lookZ: targetPos.targetZ,
1041
1041
  config: { tension: 60, friction: 20 },
1042
- immediate: prevTargetRef.current === null, // Skip animation on first render
1043
1042
  onStart: () => {
1044
1043
  isAnimatingRef.current = true;
1045
1044
  },
1046
1045
  onRest: () => {
1047
1046
  isAnimatingRef.current = false;
1048
1047
  },
1049
- });
1048
+ }));
1050
1049
 
1051
- // Track target changes
1050
+ // When targetPos changes after initial, animate to new position
1052
1051
  useEffect(() => {
1053
- prevTargetRef.current = targetKey;
1054
- }, [targetKey]);
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]);
1055
1067
 
1056
- // Update camera each frame while animating or on first frame
1068
+ // Update camera each frame
1057
1069
  useFrame(() => {
1070
+ frameCount.current++;
1071
+
1072
+ // Skip first 2 frames to ensure OrbitControls is fully initialized
1073
+ if (frameCount.current < 3) return;
1058
1074
  if (!controlsRef.current) return;
1059
1075
 
1060
- // On first frame or while animating, update camera from spring values
1061
- if (prevTargetRef.current === null || isAnimatingRef.current) {
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();
1081
+
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
+ }
1095
+
1096
+ // Subsequent frames: only update during animations
1097
+ if (isAnimatingRef.current) {
1062
1098
  camera.position.set(camX.get(), camY.get(), camZ.get());
1063
1099
  controlsRef.current.target.set(lookX.get(), lookY.get(), lookZ.get());
1064
1100
  controlsRef.current.update();
@@ -1066,23 +1102,18 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }: AnimatedCameraProps)
1066
1102
  });
1067
1103
 
1068
1104
  const resetToInitial = useCallback(() => {
1069
- const targetHeight = isFlat ? citySize * 1.5 : citySize * 1.1;
1070
- const targetZ = isFlat ? 0 : citySize * 1.3;
1071
-
1072
- camera.position.set(0, targetHeight, targetZ);
1073
- camera.lookAt(0, 0, 0);
1074
-
1075
- if (controlsRef.current) {
1076
- controlsRef.current.target.set(0, 0, 0);
1077
- controlsRef.current.update();
1078
- }
1079
- }, [isFlat, citySize, camera]);
1080
-
1081
- useEffect(() => {
1082
- if (!focusTarget) {
1083
- resetToInitial();
1084
- }
1085
- }, [resetToInitial, focusTarget]);
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;