@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":"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;
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
|
590
|
-
const targetHeight =
|
|
591
|
-
const targetZ =
|
|
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,
|
|
601
|
-
//
|
|
602
|
-
|
|
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
|
-
//
|
|
628
|
-
|
|
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
|
-
|
|
637
|
+
frameCount.current++;
|
|
638
|
+
// Skip first 2 frames to ensure OrbitControls is fully initialized
|
|
639
|
+
if (frameCount.current < 3)
|
|
631
640
|
return;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
}
|
|
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
|
@@ -491,15 +491,18 @@ function InstancedBuildings({
|
|
|
491
491
|
buildings.forEach((building, index) => {
|
|
492
492
|
let shouldCollapse = false;
|
|
493
493
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
//
|
|
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
|
|
1018
|
-
const targetHeight =
|
|
1019
|
-
const targetZ =
|
|
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,
|
|
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
|
|
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
|
|
1059
|
-
// Once animation settles, let OrbitControls handle user interaction
|
|
1068
|
+
// Update camera each frame
|
|
1060
1069
|
useFrame(() => {
|
|
1061
|
-
|
|
1070
|
+
frameCount.current++;
|
|
1062
1071
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
controlsRef.current
|
|
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
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
1076
|
-
|
|
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
|
-
}
|
|
1102
|
+
});
|
|
1080
1103
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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;
|