@principal-ai/file-city-react 0.5.49 → 0.5.51
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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"}
|
|
@@ -203,7 +203,7 @@ function getHighlightForPath(path, layers) {
|
|
|
203
203
|
function hasActiveHighlights(layers) {
|
|
204
204
|
return layers.some(layer => layer.enabled && layer.items.length > 0);
|
|
205
205
|
}
|
|
206
|
-
function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springDuration, heightMultipliersRef, }) {
|
|
206
|
+
function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springDuration, heightMultipliersRef, hiddenRef, }) {
|
|
207
207
|
const meshRef = useRef(null);
|
|
208
208
|
const startTimeRef = useRef(null);
|
|
209
209
|
const tempObject = useMemo(() => new THREE.Object3D(), []);
|
|
@@ -234,6 +234,14 @@ function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springD
|
|
|
234
234
|
const animStartTime = startTimeRef.current ?? currentTime;
|
|
235
235
|
edgeData.forEach((edge, idx) => {
|
|
236
236
|
const { x, z, fullHeight, staggerDelayMs, buildingIndex } = edge;
|
|
237
|
+
const isHidden = hiddenRef?.current?.[buildingIndex] === 1;
|
|
238
|
+
if (isHidden) {
|
|
239
|
+
tempObject.position.set(x, baseOffset, z);
|
|
240
|
+
tempObject.scale.set(0, 0, 0);
|
|
241
|
+
tempObject.updateMatrix();
|
|
242
|
+
meshRef.current.setMatrixAt(idx, tempObject.matrix);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
237
245
|
// Get height multiplier from shared ref (for collapse animation)
|
|
238
246
|
const heightMultiplier = heightMultipliersRef.current?.[buildingIndex] ?? 1;
|
|
239
247
|
// Calculate per-building animation progress
|
|
@@ -438,7 +446,7 @@ function isPathInDirectory(path, directory) {
|
|
|
438
446
|
return true;
|
|
439
447
|
return path === directory || path.startsWith(directory + '/');
|
|
440
448
|
}
|
|
441
|
-
function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hoveredIndex, selectedIndex, growProgress, animationConfig, heightScaling, linearScale, flatPatterns, staggerIndices, focusDirectory, highlightLayers, isolationMode, defaultBuildingColor, }) {
|
|
449
|
+
function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hoveredIndex, selectedIndex, growProgress, animationConfig, heightScaling, linearScale, flatPatterns, staggerIndices, focusDirectory, highlightLayers, visibilityLayers, isolationMode, defaultBuildingColor, }) {
|
|
442
450
|
const meshRef = useRef(null);
|
|
443
451
|
const startTimeRef = useRef(null);
|
|
444
452
|
const tempObject = useMemo(() => new THREE.Object3D(), []);
|
|
@@ -449,10 +457,42 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
449
457
|
// Track dim state for buildings in focus but not highlighted (0 = dimmed, 1 = full)
|
|
450
458
|
const dimMultipliersRef = useRef(null);
|
|
451
459
|
const targetDimRef = useRef(null);
|
|
452
|
-
//
|
|
460
|
+
// Track which buildings should be hidden entirely (1 = hidden, 0 = visible)
|
|
461
|
+
const hiddenRef = useRef(null);
|
|
462
|
+
// Check if user-supplied highlight layers have any active items. File-color
|
|
463
|
+
// layers don't count — they're decorative and shouldn't trigger isolation.
|
|
453
464
|
const hasActiveHighlightLayers = useMemo(() => {
|
|
454
|
-
return
|
|
455
|
-
}, [
|
|
465
|
+
return visibilityLayers.some(layer => layer.enabled && layer.items.length > 0);
|
|
466
|
+
}, [visibilityLayers]);
|
|
467
|
+
// Directories matched by a directory-type item that also contain a file-type
|
|
468
|
+
// item from any user-supplied layer. Inside these directories, a directory-
|
|
469
|
+
// only match isn't enough to count as "specifically highlighted" — file-level
|
|
470
|
+
// matches narrow the visible set, so unmatched siblings get hidden in 'hide'
|
|
471
|
+
// isolation mode.
|
|
472
|
+
const narrowedDirectories = useMemo(() => {
|
|
473
|
+
const dirs = [];
|
|
474
|
+
const files = [];
|
|
475
|
+
for (const layer of visibilityLayers) {
|
|
476
|
+
if (!layer.enabled)
|
|
477
|
+
continue;
|
|
478
|
+
for (const item of layer.items) {
|
|
479
|
+
if (item.type === 'directory')
|
|
480
|
+
dirs.push(item.path);
|
|
481
|
+
else if (item.type === 'file')
|
|
482
|
+
files.push(item.path);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const narrowed = new Set();
|
|
486
|
+
for (const dir of dirs) {
|
|
487
|
+
for (const f of files) {
|
|
488
|
+
if (f === dir || f.startsWith(dir + '/')) {
|
|
489
|
+
narrowed.add(dir);
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return narrowed;
|
|
495
|
+
}, [visibilityLayers]);
|
|
456
496
|
// Initialize height and dim multiplier arrays
|
|
457
497
|
useEffect(() => {
|
|
458
498
|
if (buildings.length > 0) {
|
|
@@ -462,39 +502,55 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
462
502
|
targetMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
463
503
|
dimMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
464
504
|
targetDimRef.current = new Float32Array(buildings.length).fill(1);
|
|
505
|
+
hiddenRef.current = new Uint8Array(buildings.length);
|
|
465
506
|
}
|
|
466
507
|
}
|
|
467
508
|
}, [buildings.length]);
|
|
468
509
|
// Update target multipliers when focusDirectory or highlightLayers change
|
|
469
510
|
useEffect(() => {
|
|
470
|
-
if (!targetMultipliersRef.current || !targetDimRef.current)
|
|
511
|
+
if (!targetMultipliersRef.current || !targetDimRef.current || !hiddenRef.current)
|
|
471
512
|
return;
|
|
472
513
|
buildings.forEach((building, index) => {
|
|
473
514
|
let shouldCollapse = false;
|
|
474
515
|
let shouldDim = false;
|
|
516
|
+
let shouldHide = false;
|
|
475
517
|
const isInFocusDirectory = focusDirectory
|
|
476
518
|
? isPathInDirectory(building.path, focusDirectory)
|
|
477
519
|
: true; // No focusDirectory means all are "in focus"
|
|
520
|
+
const layerMatches = hasActiveHighlightLayers
|
|
521
|
+
? getLayerMatchesForPath(building.path, visibilityLayers)
|
|
522
|
+
: [];
|
|
478
523
|
const isHighlighted = hasActiveHighlightLayers
|
|
479
|
-
?
|
|
524
|
+
? layerMatches.length > 0
|
|
480
525
|
: true; // No highlights means all are "highlighted"
|
|
481
|
-
//
|
|
526
|
+
// For 'hide' mode, a directory-only match doesn't count as highlighted
|
|
527
|
+
// when that directory has been narrowed by file-level matches in some
|
|
528
|
+
// layer — the file-level matches define the visible subset.
|
|
529
|
+
const isSpecificallyHighlighted = hasActiveHighlightLayers
|
|
530
|
+
? layerMatches.some(m => m.item.type === 'file' || !narrowedDirectories.has(m.item.path))
|
|
531
|
+
: true;
|
|
532
|
+
// Determine collapse/dim/hide behavior based on what's active:
|
|
482
533
|
// - focusDirectory only: collapse if outside focus
|
|
483
534
|
// - highlightLayers only (with collapse mode): collapse if not highlighted
|
|
484
|
-
// -
|
|
535
|
+
// - highlightLayers only (with hide mode): hide if not specifically highlighted
|
|
536
|
+
// - both: collapse if outside focus, dim/hide if in focus but not highlighted
|
|
485
537
|
if (focusDirectory && hasActiveHighlightLayers && isolationMode === 'collapse') {
|
|
486
|
-
// Both active: collapse if outside focus, dim if in focus but not highlighted
|
|
487
538
|
shouldCollapse = !isInFocusDirectory;
|
|
488
539
|
shouldDim = isInFocusDirectory && !isHighlighted;
|
|
489
540
|
}
|
|
541
|
+
else if (focusDirectory && hasActiveHighlightLayers && isolationMode === 'hide') {
|
|
542
|
+
shouldCollapse = !isInFocusDirectory;
|
|
543
|
+
shouldHide = isInFocusDirectory && !isSpecificallyHighlighted;
|
|
544
|
+
}
|
|
490
545
|
else if (focusDirectory) {
|
|
491
|
-
// Focus only: collapse if outside focus directory
|
|
492
546
|
shouldCollapse = !isInFocusDirectory;
|
|
493
547
|
}
|
|
494
548
|
else if (hasActiveHighlightLayers && isolationMode === 'collapse') {
|
|
495
|
-
// Highlight only with collapse: collapse if not highlighted
|
|
496
549
|
shouldCollapse = !isHighlighted;
|
|
497
550
|
}
|
|
551
|
+
else if (hasActiveHighlightLayers && isolationMode === 'hide') {
|
|
552
|
+
shouldHide = !isSpecificallyHighlighted;
|
|
553
|
+
}
|
|
498
554
|
// Height: 1.0 = full, 0.05 = flat (collapsed or dimmed)
|
|
499
555
|
if (shouldCollapse || shouldDim) {
|
|
500
556
|
targetMultipliersRef.current[index] = 0.05;
|
|
@@ -505,8 +561,10 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
505
561
|
// Dim ref controls graying: 0 = gray out, 1 = keep color
|
|
506
562
|
// Collapsed buildings go gray, dimmed buildings keep their color
|
|
507
563
|
targetDimRef.current[index] = shouldCollapse ? 0 : 1;
|
|
564
|
+
// Hidden ref controls full invisibility (mesh + edges + icon)
|
|
565
|
+
hiddenRef.current[index] = shouldHide ? 1 : 0;
|
|
508
566
|
});
|
|
509
|
-
}, [focusDirectory, buildings,
|
|
567
|
+
}, [focusDirectory, buildings, visibilityLayers, isolationMode, hasActiveHighlightLayers, narrowedDirectories]);
|
|
510
568
|
// Pre-compute building data
|
|
511
569
|
const buildingData = useMemo(() => {
|
|
512
570
|
return buildings.map((building, index) => {
|
|
@@ -588,6 +646,14 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
588
646
|
const collapseSpeed = 0.08;
|
|
589
647
|
buildingData.forEach((data, instanceIndex) => {
|
|
590
648
|
const { width, depth, fullHeight, x, z, staggerDelayMs } = data;
|
|
649
|
+
const isHidden = hiddenRef.current?.[instanceIndex] === 1;
|
|
650
|
+
if (isHidden) {
|
|
651
|
+
tempObject.position.set(x, baseOffset, z);
|
|
652
|
+
tempObject.scale.set(0, 0, 0);
|
|
653
|
+
tempObject.updateMatrix();
|
|
654
|
+
meshRef.current.setMatrixAt(instanceIndex, tempObject.matrix);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
591
657
|
// Animate height multiplier towards target
|
|
592
658
|
const currentMultiplier = heightMultipliersRef.current[instanceIndex];
|
|
593
659
|
const targetMultiplier = targetMultipliersRef.current[instanceIndex];
|
|
@@ -671,7 +737,7 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
671
737
|
z: d.z,
|
|
672
738
|
staggerDelayMs: d.staggerDelayMs,
|
|
673
739
|
buildingIndex: d.index,
|
|
674
|
-
})), growProgress: growProgress, minHeight: minHeight, baseOffset: baseOffset, springDuration: springDuration, heightMultipliersRef: heightMultipliersRef }), _jsx(BorderHighlights, { buildings: buildings, centerOffset: centerOffset, highlightLayers: highlightLayers, growProgress: growProgress, minHeight: minHeight, baseOffset: baseOffset, springDuration: springDuration, heightMultipliersRef: heightMultipliersRef, heightScaling: heightScaling, linearScale: linearScale, flatPatterns: flatPatterns, staggerIndices: staggerIndices, animationConfig: animationConfig })] }));
|
|
740
|
+
})), growProgress: growProgress, minHeight: minHeight, baseOffset: baseOffset, springDuration: springDuration, heightMultipliersRef: heightMultipliersRef, hiddenRef: hiddenRef }), _jsx(BorderHighlights, { buildings: buildings, centerOffset: centerOffset, highlightLayers: highlightLayers, growProgress: growProgress, minHeight: minHeight, baseOffset: baseOffset, springDuration: springDuration, heightMultipliersRef: heightMultipliersRef, heightScaling: heightScaling, linearScale: linearScale, flatPatterns: flatPatterns, staggerIndices: staggerIndices, animationConfig: animationConfig })] }));
|
|
675
741
|
}
|
|
676
742
|
function AnimatedIcon({ x, z, targetHeight, iconSize, texture, opacity, growProgress, }) {
|
|
677
743
|
const meshRef = useRef(null);
|
|
@@ -699,7 +765,33 @@ function AnimatedIcon({ x, z, targetHeight, iconSize, texture, opacity, growProg
|
|
|
699
765
|
});
|
|
700
766
|
return (_jsxs("mesh", { ref: meshRef, position: [x, 0, z], scale: [iconSize, iconSize, 1], raycast: () => null, children: [_jsx("planeGeometry", { args: [1, 1] }), _jsx("meshBasicMaterial", { ref: materialRef, map: texture, transparent: true, opacity: 0.8, depthTest: true, depthWrite: false, side: THREE.DoubleSide })] }));
|
|
701
767
|
}
|
|
702
|
-
function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, linearScale, flatPatterns, highlightLayers, isolationMode, hasActiveHighlights, }) {
|
|
768
|
+
function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, linearScale, flatPatterns, highlightLayers, visibilityLayers, isolationMode, hasActiveHighlights, }) {
|
|
769
|
+
// Same narrowing rule as InstancedBuildings, scoped to user highlight layers
|
|
770
|
+
// only (file-color layers don't narrow visibility).
|
|
771
|
+
const narrowedDirectories = useMemo(() => {
|
|
772
|
+
const dirs = [];
|
|
773
|
+
const files = [];
|
|
774
|
+
for (const layer of visibilityLayers) {
|
|
775
|
+
if (!layer.enabled)
|
|
776
|
+
continue;
|
|
777
|
+
for (const item of layer.items) {
|
|
778
|
+
if (item.type === 'directory')
|
|
779
|
+
dirs.push(item.path);
|
|
780
|
+
else if (item.type === 'file')
|
|
781
|
+
files.push(item.path);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const narrowed = new Set();
|
|
785
|
+
for (const dir of dirs) {
|
|
786
|
+
for (const f of files) {
|
|
787
|
+
if (f === dir || f.startsWith(dir + '/')) {
|
|
788
|
+
narrowed.add(dir);
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return narrowed;
|
|
794
|
+
}, [visibilityLayers]);
|
|
703
795
|
// Pre-compute buildings with icons
|
|
704
796
|
const buildingsWithIcons = useMemo(() => {
|
|
705
797
|
return buildings
|
|
@@ -707,10 +799,11 @@ function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, l
|
|
|
707
799
|
const config = getConfigForFile(building);
|
|
708
800
|
if (!config.icon)
|
|
709
801
|
return null;
|
|
710
|
-
const
|
|
711
|
-
const isHighlighted =
|
|
802
|
+
const matches = getLayerMatchesForPath(building.path, visibilityLayers);
|
|
803
|
+
const isHighlighted = matches.length > 0;
|
|
804
|
+
const isSpecificallyHighlighted = matches.some(m => m.item.type === 'file' || !narrowedDirectories.has(m.item.path));
|
|
712
805
|
const shouldDim = hasActiveHighlights && !isHighlighted;
|
|
713
|
-
const shouldHide =
|
|
806
|
+
const shouldHide = hasActiveHighlights && isolationMode === 'hide' && !isSpecificallyHighlighted;
|
|
714
807
|
const shouldCollapse = shouldDim && isolationMode === 'collapse';
|
|
715
808
|
// Hide icons for buildings that are hidden or collapsed
|
|
716
809
|
if (shouldHide || shouldCollapse)
|
|
@@ -732,12 +825,13 @@ function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, l
|
|
|
732
825
|
}, [
|
|
733
826
|
buildings,
|
|
734
827
|
centerOffset,
|
|
735
|
-
|
|
828
|
+
visibilityLayers,
|
|
736
829
|
isolationMode,
|
|
737
830
|
hasActiveHighlights,
|
|
738
831
|
heightScaling,
|
|
739
832
|
linearScale,
|
|
740
833
|
flatPatterns,
|
|
834
|
+
narrowedDirectories,
|
|
741
835
|
]);
|
|
742
836
|
// Icons are now always rendered (flat or grown)
|
|
743
837
|
return (_jsx(_Fragment, { children: buildingsWithIcons.map(({ building, config, x, z, targetHeight, shouldDim }) => {
|
|
@@ -777,7 +871,7 @@ function DistrictFloor({ district, centerOffset, highlightColor, growProgress })
|
|
|
777
871
|
const flatZ = depth / 2 - 6; // Near bottom of district when flat, with padding
|
|
778
872
|
const grownZ = depth / 2 + 2; // Just outside edge when grown
|
|
779
873
|
const textZ = flatZ + (grownZ - flatZ) * growProgress;
|
|
780
|
-
return (_jsxs("group", { position: [centerX, 0, centerZ], children: [_jsxs("lineSegments", { rotation: [-Math.PI / 2, 0, 0], position: [0, floorY, 0], renderOrder: -1, children: [_jsx("edgesGeometry", { args: [new THREE.PlaneGeometry(width, depth)], attach: "geometry" }), _jsx("lineBasicMaterial", { color: borderColor, linewidth: lineWidth, depthWrite: false })] }),
|
|
874
|
+
return (_jsxs("group", { position: [centerX, 0, centerZ], children: [_jsxs("lineSegments", { rotation: [-Math.PI / 2, 0, 0], position: [0, floorY, 0], renderOrder: -1, children: [_jsx("edgesGeometry", { args: [new THREE.PlaneGeometry(width, depth)], attach: "geometry" }), _jsx("lineBasicMaterial", { color: borderColor, linewidth: lineWidth, depthWrite: false })] }), _jsx(Text, { position: [0, textY, textZ], rotation: [textRotationX, 0, 0], fontSize: Math.max(6, Math.min(12, width / 3)), color: labelColor, anchorX: "center", anchorY: "middle", outlineWidth: 0.15, outlineColor: "#0f172a", children: dirName })] }));
|
|
781
875
|
}
|
|
782
876
|
export const DEFAULT_CAMERA_CONTROLS = {
|
|
783
877
|
leftDrag: 'pan',
|
|
@@ -957,7 +1051,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
957
1051
|
// Calculate initial position with default aspect ratio
|
|
958
1052
|
// This will be corrected in Frame 1 if aspect is different
|
|
959
1053
|
const initialHeight = calculateFlatCameraHeight(1);
|
|
960
|
-
console.log('[Spring init] Initializing with 2D position, height:', initialHeight);
|
|
961
1054
|
return {
|
|
962
1055
|
camX: 0,
|
|
963
1056
|
camY: initialHeight,
|
|
@@ -967,17 +1060,11 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
967
1060
|
lookZ: 0,
|
|
968
1061
|
config: { tension: 60, friction: 20 },
|
|
969
1062
|
onStart: () => {
|
|
970
|
-
// Only allow animations after initial setup is complete
|
|
971
1063
|
if (hasAppliedInitial.current) {
|
|
972
|
-
console.log('[Spring onStart] Animation starting - camY:', camY.get());
|
|
973
1064
|
isAnimatingRef.current = true;
|
|
974
1065
|
}
|
|
975
|
-
else {
|
|
976
|
-
console.log('[Spring onStart] Blocked - initialization not complete');
|
|
977
|
-
}
|
|
978
1066
|
},
|
|
979
1067
|
onRest: () => {
|
|
980
|
-
console.log('[Spring onRest] Animation finished');
|
|
981
1068
|
isAnimatingRef.current = false;
|
|
982
1069
|
},
|
|
983
1070
|
};
|
|
@@ -1012,19 +1099,11 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1012
1099
|
// When isFlat changes from true to false, animate to 3D view
|
|
1013
1100
|
// Component always starts in 2D, so we only animate the 2D→3D transition
|
|
1014
1101
|
useEffect(() => {
|
|
1015
|
-
|
|
1016
|
-
// Skip until camera is initialized
|
|
1017
|
-
if (!hasAppliedInitial.current) {
|
|
1018
|
-
console.log('[useEffect] Skipping - not initialized yet');
|
|
1102
|
+
if (!hasAppliedInitial.current)
|
|
1019
1103
|
return;
|
|
1020
|
-
}
|
|
1021
|
-
// Only animate if isFlat changed from true to false (2D → 3D transition)
|
|
1022
1104
|
const isFlatChanged = prevIsFlatRef.current !== isFlat;
|
|
1023
|
-
if (!isFlatChanged)
|
|
1024
|
-
console.log('[useEffect] No isFlat change - skipping');
|
|
1105
|
+
if (!isFlatChanged)
|
|
1025
1106
|
return;
|
|
1026
|
-
}
|
|
1027
|
-
console.log('[useEffect] isFlat changed from', prevIsFlatRef.current, 'to', isFlat, '- animating transition');
|
|
1028
1107
|
prevIsFlatRef.current = isFlat;
|
|
1029
1108
|
// Calculate target position for 3D view
|
|
1030
1109
|
const newPos = isFlat
|
|
@@ -1046,7 +1125,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1046
1125
|
targetY: 0,
|
|
1047
1126
|
targetZ: 0,
|
|
1048
1127
|
};
|
|
1049
|
-
console.log('[api.start#isFlat-toggle]', newPos);
|
|
1050
1128
|
api.start({
|
|
1051
1129
|
camX: newPos.x,
|
|
1052
1130
|
camY: newPos.y,
|
|
@@ -1109,7 +1187,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1109
1187
|
targetZ: 0,
|
|
1110
1188
|
};
|
|
1111
1189
|
}
|
|
1112
|
-
console.log('[api.start#focus-target]', { focusTarget, isFlat, newPos });
|
|
1113
1190
|
api.start({
|
|
1114
1191
|
camX: newPos.x,
|
|
1115
1192
|
camY: newPos.y,
|
|
@@ -1122,7 +1199,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1122
1199
|
}, [focusTarget, isFlat]);
|
|
1123
1200
|
// Update camera each frame
|
|
1124
1201
|
useFrame(() => {
|
|
1125
|
-
var _a;
|
|
1126
1202
|
frameCount.current++;
|
|
1127
1203
|
// On Frame 1: Set camera to initial 2D position and mark as ready
|
|
1128
1204
|
// Component always starts in 2D mode, so we just need to set the correct position once
|
|
@@ -1130,20 +1206,17 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1130
1206
|
// Ensure camera FOV is correct (defaults to 75 before prop applies)
|
|
1131
1207
|
const perspCam = camera;
|
|
1132
1208
|
if (perspCam.fov !== 50) {
|
|
1133
|
-
console.log('[Frame 1] Correcting FOV from', perspCam.fov, 'to 50');
|
|
1134
1209
|
perspCam.fov = 50;
|
|
1135
1210
|
perspCam.updateProjectionMatrix();
|
|
1136
1211
|
}
|
|
1137
1212
|
// Calculate initial 2D position with correct aspect ratio
|
|
1138
1213
|
const initialPos = getInitial2DPosition();
|
|
1139
|
-
console.log('[Frame 1] Setting camera to initial 2D position:', initialPos);
|
|
1140
1214
|
camera.position.set(initialPos.x, initialPos.y, initialPos.z);
|
|
1141
1215
|
// Wait for controls to be ready, then set target and sync spring
|
|
1142
1216
|
if (controlsRef.current) {
|
|
1143
1217
|
controlsRef.current.target.set(initialPos.targetX, initialPos.targetY, initialPos.targetZ);
|
|
1144
1218
|
controlsRef.current.update();
|
|
1145
1219
|
// Sync spring to match camera position (use immediate to avoid animation)
|
|
1146
|
-
console.log('[api.start#frame1-immediate]', initialPos);
|
|
1147
1220
|
api.start({
|
|
1148
1221
|
camX: initialPos.x,
|
|
1149
1222
|
camY: initialPos.y,
|
|
@@ -1165,28 +1238,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1165
1238
|
// Wait for controls and initialization to complete
|
|
1166
1239
|
if (!controlsRef.current || !hasAppliedInitial.current)
|
|
1167
1240
|
return;
|
|
1168
|
-
// [debug] log which branch runs each frame so consumers can identify
|
|
1169
|
-
// unexpected camera mutation when no api.start fired.
|
|
1170
|
-
if (isOrbitingRef.current ||
|
|
1171
|
-
isTiltingRef.current ||
|
|
1172
|
-
isAnimatingRef.current) {
|
|
1173
|
-
const branch = isOrbitingRef.current
|
|
1174
|
-
? 'orbit'
|
|
1175
|
-
: isTiltingRef.current
|
|
1176
|
-
? 'tilt'
|
|
1177
|
-
: 'animating';
|
|
1178
|
-
const w = (_a = globalThis).__fileCityFrameLog ?? (_a.__fileCityFrameLog = new Map());
|
|
1179
|
-
const last = w.get(branch) ?? 0;
|
|
1180
|
-
const now = performance.now();
|
|
1181
|
-
if (now - last > 200) {
|
|
1182
|
-
w.set(branch, now);
|
|
1183
|
-
// eslint-disable-next-line no-console
|
|
1184
|
-
console.log(`[useFrame#${branch}]`, {
|
|
1185
|
-
camY: camY.get(),
|
|
1186
|
-
posY: camera.position.y,
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
1241
|
// Handle orbit animation (horizontal rotation along arc)
|
|
1191
1242
|
if (isOrbitingRef.current && orbitParamsRef.current) {
|
|
1192
1243
|
const { centerX, centerZ, distance, height } = orbitParamsRef.current;
|
|
@@ -1244,7 +1295,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1244
1295
|
const resetToInitial = useCallback(() => {
|
|
1245
1296
|
const targetHeight = citySize * 1.1;
|
|
1246
1297
|
const targetZ = citySize * 1.3;
|
|
1247
|
-
console.log('[api.start#resetToInitial]', { targetHeight, targetZ });
|
|
1248
1298
|
api.start({
|
|
1249
1299
|
camX: 0,
|
|
1250
1300
|
camY: targetHeight,
|
|
@@ -1258,7 +1308,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1258
1308
|
const effectiveSize = size ?? citySize * 0.3;
|
|
1259
1309
|
const distance = Math.max(effectiveSize * 2, 50);
|
|
1260
1310
|
const height = Math.max(effectiveSize * 1.5, 40);
|
|
1261
|
-
console.log('[api.start#moveTo]', { x, z, height, distance });
|
|
1262
1311
|
api.start({
|
|
1263
1312
|
camX: x,
|
|
1264
1313
|
camY: height,
|
|
@@ -1288,7 +1337,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1288
1337
|
});
|
|
1289
1338
|
}
|
|
1290
1339
|
const config = options?.duration ? { duration: options.duration } : undefined;
|
|
1291
|
-
console.log('[api.start#setFlatView]', { x, z, height, options });
|
|
1292
1340
|
api.start({
|
|
1293
1341
|
camX: x,
|
|
1294
1342
|
camY: height,
|
|
@@ -1316,7 +1364,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1316
1364
|
const animConfig = options?.duration
|
|
1317
1365
|
? { duration: options.duration, easing: (t) => t }
|
|
1318
1366
|
: { tension: 60, friction: 20 };
|
|
1319
|
-
console.log('[api.start#setTarget]', { x, y, z, newCamX, newCamY, newCamZ });
|
|
1320
1367
|
api.start({
|
|
1321
1368
|
camX: newCamX,
|
|
1322
1369
|
camY: newCamY,
|
|
@@ -1764,7 +1811,7 @@ function SelectionRing({ district, centerOffset, color, borderWidth, growProgres
|
|
|
1764
1811
|
const barH = 0.5;
|
|
1765
1812
|
return (_jsxs("group", { position: [cx, y, cz], children: [_jsxs("mesh", { position: [0, 0, -d / 2], renderOrder: 20, children: [_jsx("boxGeometry", { args: [w + t, barH, t] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.95 })] }), _jsxs("mesh", { position: [0, 0, d / 2], renderOrder: 20, children: [_jsx("boxGeometry", { args: [w + t, barH, t] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.95 })] }), _jsxs("mesh", { position: [-w / 2, 0, 0], renderOrder: 20, children: [_jsx("boxGeometry", { args: [t, barH, d + t] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.95 })] }), _jsxs("mesh", { position: [w / 2, 0, 0], renderOrder: 20, children: [_jsx("boxGeometry", { args: [t, barH, d + t] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.95 })] })] }));
|
|
1766
1813
|
}
|
|
1767
|
-
function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding, selectedBuilding, selectedDistrict, selectionStyle, growProgress, animationConfig, highlightLayers, isolationMode, heightScaling, linearScale, flatPatterns, focusDirectory, focusColor, adaptCameraToBuildings = false, elevatedScopePanels, dismissingPanelIds, onPanelDismissed, cameraControls, defaultBuildingColor, onCameraReady, }) {
|
|
1814
|
+
function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding, selectedBuilding, selectedDistrict, selectionStyle, growProgress, animationConfig, highlightLayers, visibilityLayers, isolationMode, heightScaling, linearScale, flatPatterns, focusDirectory, focusColor, adaptCameraToBuildings = false, elevatedScopePanels, dismissingPanelIds, onPanelDismissed, cameraControls, defaultBuildingColor, onCameraReady, }) {
|
|
1768
1815
|
const centerOffset = useMemo(() => ({
|
|
1769
1816
|
x: (cityData.bounds.minX + cityData.bounds.maxX) / 2,
|
|
1770
1817
|
z: (cityData.bounds.minZ + cityData.bounds.maxZ) / 2,
|
|
@@ -1776,7 +1823,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
1776
1823
|
return 0;
|
|
1777
1824
|
return Math.max(...cityData.buildings.map(b => b.dimensions[1]), 0);
|
|
1778
1825
|
}, [adaptCameraToBuildings, cityData.buildings]);
|
|
1779
|
-
const activeHighlights = useMemo(() => hasActiveHighlights(
|
|
1826
|
+
const activeHighlights = useMemo(() => hasActiveHighlights(visibilityLayers), [visibilityLayers]);
|
|
1780
1827
|
// Helper to check if a path is inside a directory
|
|
1781
1828
|
const isPathInDirectory = useCallback((path, directory) => {
|
|
1782
1829
|
if (!directory)
|
|
@@ -1968,7 +2015,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
1968
2015
|
// Focus color takes priority, then highlight layer color
|
|
1969
2016
|
const districtColor = (isFocused && buildingFocusColor) ? buildingFocusColor : highlightLayerColor;
|
|
1970
2017
|
return (_jsx(DistrictFloor, { district: district, centerOffset: centerOffset, opacity: 1, highlightColor: districtColor, growProgress: growProgress }, district.path));
|
|
1971
|
-
}), _jsx(InstancedBuildings, { buildings: cityData.buildings, centerOffset: centerOffset, onHover: onBuildingHover, onClick: onBuildingClick, hoveredIndex: hoveredIndex, selectedIndex: selectedIndex, growProgress: growProgress, animationConfig: animationConfig, heightScaling: heightScaling, linearScale: linearScale, flatPatterns: flatPatterns, staggerIndices: staggerIndices, focusDirectory: buildingFocusDirectory, highlightLayers: highlightLayers, isolationMode: isolationMode, defaultBuildingColor: defaultBuildingColor }), _jsx(BuildingIcons, { buildings: cityData.buildings, centerOffset: centerOffset, growProgress: growProgress, heightScaling: heightScaling, linearScale: linearScale, flatPatterns: flatPatterns, highlightLayers: highlightLayers, isolationMode: isolationMode, hasActiveHighlights: activeHighlights }), growProgress === 0 &&
|
|
2018
|
+
}), _jsx(InstancedBuildings, { buildings: cityData.buildings, centerOffset: centerOffset, onHover: onBuildingHover, onClick: onBuildingClick, hoveredIndex: hoveredIndex, selectedIndex: selectedIndex, growProgress: growProgress, animationConfig: animationConfig, heightScaling: heightScaling, linearScale: linearScale, flatPatterns: flatPatterns, staggerIndices: staggerIndices, focusDirectory: buildingFocusDirectory, highlightLayers: highlightLayers, visibilityLayers: visibilityLayers, isolationMode: isolationMode, defaultBuildingColor: defaultBuildingColor }), _jsx(BuildingIcons, { buildings: cityData.buildings, centerOffset: centerOffset, growProgress: growProgress, heightScaling: heightScaling, linearScale: linearScale, flatPatterns: flatPatterns, highlightLayers: highlightLayers, visibilityLayers: visibilityLayers, isolationMode: isolationMode, hasActiveHighlights: activeHighlights }), growProgress === 0 &&
|
|
1972
2019
|
elevatedScopePanels?.map(panel => (_jsx(ElevatedScopePanelMesh, { panel: panel, centerOffset: centerOffset, dismissing: dismissingPanelIds?.has(panel.id) ?? false, onDismissed: onPanelDismissed }, panel.id))), selectedDistrict && (_jsx(SelectionRing, { district: selectedDistrict, centerOffset: centerOffset, color: selectionStyle?.color ?? '#facc15', borderWidth: selectionStyle?.borderWidth ?? 2, growProgress: growProgress }))] }));
|
|
1973
2020
|
}
|
|
1974
2021
|
/**
|
|
@@ -2018,6 +2065,9 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
2018
2065
|
const focusDirectory = resolved.focusDirectory;
|
|
2019
2066
|
const focusColor = resolved.focusColor;
|
|
2020
2067
|
const isolationMode = resolved.isolationMode;
|
|
2068
|
+
// User-supplied highlight layers only — used for visibility decisions so
|
|
2069
|
+
// file-color layers don't keep every building visible in 'hide' mode.
|
|
2070
|
+
const visibilityLayers = useMemo(() => (externalHighlightLayers ?? []), [externalHighlightLayers]);
|
|
2021
2071
|
// `selectedPath` wins over the deprecated `selectedBuilding` when both are
|
|
2022
2072
|
// set. A path resolves to either a building (file selection) or a district
|
|
2023
2073
|
// (directory selection) — never both.
|
|
@@ -2101,6 +2151,6 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
2101
2151
|
height: '100%',
|
|
2102
2152
|
opacity: cameraReady ? 1 : 0,
|
|
2103
2153
|
transition: 'opacity 0.1s ease-in',
|
|
2104
|
-
}, children: [_jsx(CityScene, { cityData: cityData, onBuildingHover: handleBuildingHover, onBuildingClick: onBuildingClick, hoveredBuilding: hoveredBuilding, selectedBuilding: resolvedSelection.building, selectedDistrict: resolvedSelection.district, selectionStyle: selectionStyle, growProgress: growProgress, animationConfig: animationConfig, highlightLayers: highlightLayers, isolationMode: isolationMode, heightScaling: heightScaling, linearScale: linearScale, flatPatterns: flatPatterns, focusDirectory: focusDirectory, focusColor: focusColor, adaptCameraToBuildings: adaptCameraToBuildings, elevatedScopePanels: elevatedScopePanels, dismissingPanelIds: dismissingPanelIds, onPanelDismissed: onPanelDismissed, cameraControls: cameraControls, defaultBuildingColor: defaultBuildingColor, onCameraReady: () => setCameraReady(true) }), onCameraFrame && _jsx(CameraFrameBridge, { onCameraFrame: onCameraFrame })] }), _jsx(InfoPanel, { building: resolvedSelection.building }), showControls && (_jsx(ControlsOverlay, { isFlat: !isGrown, onToggle: handleToggle, onResetCamera: resetCamera, onLookDown: () => tiltCameraTo(0) }))] }));
|
|
2154
|
+
}, children: [_jsx(CityScene, { cityData: cityData, onBuildingHover: handleBuildingHover, onBuildingClick: onBuildingClick, hoveredBuilding: hoveredBuilding, selectedBuilding: resolvedSelection.building, selectedDistrict: resolvedSelection.district, selectionStyle: selectionStyle, growProgress: growProgress, animationConfig: animationConfig, highlightLayers: highlightLayers, visibilityLayers: visibilityLayers, isolationMode: isolationMode, heightScaling: heightScaling, linearScale: linearScale, flatPatterns: flatPatterns, focusDirectory: focusDirectory, focusColor: focusColor, adaptCameraToBuildings: adaptCameraToBuildings, elevatedScopePanels: elevatedScopePanels, dismissingPanelIds: dismissingPanelIds, onPanelDismissed: onPanelDismissed, cameraControls: cameraControls, defaultBuildingColor: defaultBuildingColor, onCameraReady: () => setCameraReady(true) }), onCameraFrame && _jsx(CameraFrameBridge, { onCameraFrame: onCameraFrame })] }), _jsx(InfoPanel, { building: resolvedSelection.building }), showControls && (_jsx(ControlsOverlay, { isFlat: !isGrown, onToggle: handleToggle, onResetCamera: resetCamera, onLookDown: () => tiltCameraTo(0) }))] }));
|
|
2105
2155
|
}
|
|
2106
2156
|
export default FileCity3D;
|
package/package.json
CHANGED
|
@@ -409,6 +409,7 @@ interface BuildingEdgesProps {
|
|
|
409
409
|
baseOffset: number;
|
|
410
410
|
springDuration: number;
|
|
411
411
|
heightMultipliersRef: React.MutableRefObject<Float32Array | null>;
|
|
412
|
+
hiddenRef?: React.MutableRefObject<Uint8Array | null>;
|
|
412
413
|
}
|
|
413
414
|
|
|
414
415
|
function BuildingEdges({
|
|
@@ -418,6 +419,7 @@ function BuildingEdges({
|
|
|
418
419
|
baseOffset,
|
|
419
420
|
springDuration,
|
|
420
421
|
heightMultipliersRef,
|
|
422
|
+
hiddenRef,
|
|
421
423
|
}: BuildingEdgesProps) {
|
|
422
424
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
|
423
425
|
const startTimeRef = useRef<number | null>(null);
|
|
@@ -456,6 +458,16 @@ function BuildingEdges({
|
|
|
456
458
|
edgeData.forEach((edge, idx) => {
|
|
457
459
|
const { x, z, fullHeight, staggerDelayMs, buildingIndex } = edge;
|
|
458
460
|
|
|
461
|
+
const isHidden = hiddenRef?.current?.[buildingIndex] === 1;
|
|
462
|
+
|
|
463
|
+
if (isHidden) {
|
|
464
|
+
tempObject.position.set(x, baseOffset, z);
|
|
465
|
+
tempObject.scale.set(0, 0, 0);
|
|
466
|
+
tempObject.updateMatrix();
|
|
467
|
+
meshRef.current!.setMatrixAt(idx, tempObject.matrix);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
459
471
|
// Get height multiplier from shared ref (for collapse animation)
|
|
460
472
|
const heightMultiplier = heightMultipliersRef.current?.[buildingIndex] ?? 1;
|
|
461
473
|
|
|
@@ -759,7 +771,10 @@ interface InstancedBuildingsProps {
|
|
|
759
771
|
flatPatterns: FlatPattern[];
|
|
760
772
|
staggerIndices: number[];
|
|
761
773
|
focusDirectory: string | null;
|
|
774
|
+
/** Combined layers (user highlights + filtered file-color layers) — used for fill colors. */
|
|
762
775
|
highlightLayers: HighlightLayer[];
|
|
776
|
+
/** User-supplied highlight layers only (no file-color layers) — used for visibility decisions. */
|
|
777
|
+
visibilityLayers: HighlightLayer[];
|
|
763
778
|
isolationMode: IsolationMode;
|
|
764
779
|
defaultBuildingColor?: string;
|
|
765
780
|
}
|
|
@@ -785,6 +800,7 @@ function InstancedBuildings({
|
|
|
785
800
|
staggerIndices,
|
|
786
801
|
focusDirectory,
|
|
787
802
|
highlightLayers,
|
|
803
|
+
visibilityLayers,
|
|
788
804
|
isolationMode,
|
|
789
805
|
defaultBuildingColor,
|
|
790
806
|
}: InstancedBuildingsProps) {
|
|
@@ -799,11 +815,41 @@ function InstancedBuildings({
|
|
|
799
815
|
// Track dim state for buildings in focus but not highlighted (0 = dimmed, 1 = full)
|
|
800
816
|
const dimMultipliersRef = useRef<Float32Array | null>(null);
|
|
801
817
|
const targetDimRef = useRef<Float32Array | null>(null);
|
|
818
|
+
// Track which buildings should be hidden entirely (1 = hidden, 0 = visible)
|
|
819
|
+
const hiddenRef = useRef<Uint8Array | null>(null);
|
|
802
820
|
|
|
803
|
-
// Check if highlight layers have any active items
|
|
821
|
+
// Check if user-supplied highlight layers have any active items. File-color
|
|
822
|
+
// layers don't count — they're decorative and shouldn't trigger isolation.
|
|
804
823
|
const hasActiveHighlightLayers = useMemo(() => {
|
|
805
|
-
return
|
|
806
|
-
}, [
|
|
824
|
+
return visibilityLayers.some(layer => layer.enabled && layer.items.length > 0);
|
|
825
|
+
}, [visibilityLayers]);
|
|
826
|
+
|
|
827
|
+
// Directories matched by a directory-type item that also contain a file-type
|
|
828
|
+
// item from any user-supplied layer. Inside these directories, a directory-
|
|
829
|
+
// only match isn't enough to count as "specifically highlighted" — file-level
|
|
830
|
+
// matches narrow the visible set, so unmatched siblings get hidden in 'hide'
|
|
831
|
+
// isolation mode.
|
|
832
|
+
const narrowedDirectories = useMemo(() => {
|
|
833
|
+
const dirs: string[] = [];
|
|
834
|
+
const files: string[] = [];
|
|
835
|
+
for (const layer of visibilityLayers) {
|
|
836
|
+
if (!layer.enabled) continue;
|
|
837
|
+
for (const item of layer.items) {
|
|
838
|
+
if (item.type === 'directory') dirs.push(item.path);
|
|
839
|
+
else if (item.type === 'file') files.push(item.path);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const narrowed = new Set<string>();
|
|
843
|
+
for (const dir of dirs) {
|
|
844
|
+
for (const f of files) {
|
|
845
|
+
if (f === dir || f.startsWith(dir + '/')) {
|
|
846
|
+
narrowed.add(dir);
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return narrowed;
|
|
852
|
+
}, [visibilityLayers]);
|
|
807
853
|
|
|
808
854
|
// Initialize height and dim multiplier arrays
|
|
809
855
|
useEffect(() => {
|
|
@@ -816,40 +862,57 @@ function InstancedBuildings({
|
|
|
816
862
|
targetMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
817
863
|
dimMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
818
864
|
targetDimRef.current = new Float32Array(buildings.length).fill(1);
|
|
865
|
+
hiddenRef.current = new Uint8Array(buildings.length);
|
|
819
866
|
}
|
|
820
867
|
}
|
|
821
868
|
}, [buildings.length]);
|
|
822
869
|
|
|
823
870
|
// Update target multipliers when focusDirectory or highlightLayers change
|
|
824
871
|
useEffect(() => {
|
|
825
|
-
if (!targetMultipliersRef.current || !targetDimRef.current) return;
|
|
872
|
+
if (!targetMultipliersRef.current || !targetDimRef.current || !hiddenRef.current) return;
|
|
826
873
|
|
|
827
874
|
buildings.forEach((building, index) => {
|
|
828
875
|
let shouldCollapse = false;
|
|
829
876
|
let shouldDim = false;
|
|
877
|
+
let shouldHide = false;
|
|
830
878
|
|
|
831
879
|
const isInFocusDirectory = focusDirectory
|
|
832
880
|
? isPathInDirectory(building.path, focusDirectory)
|
|
833
881
|
: true; // No focusDirectory means all are "in focus"
|
|
834
882
|
|
|
883
|
+
const layerMatches = hasActiveHighlightLayers
|
|
884
|
+
? getLayerMatchesForPath(building.path, visibilityLayers)
|
|
885
|
+
: [];
|
|
835
886
|
const isHighlighted = hasActiveHighlightLayers
|
|
836
|
-
?
|
|
887
|
+
? layerMatches.length > 0
|
|
837
888
|
: true; // No highlights means all are "highlighted"
|
|
838
889
|
|
|
839
|
-
//
|
|
890
|
+
// For 'hide' mode, a directory-only match doesn't count as highlighted
|
|
891
|
+
// when that directory has been narrowed by file-level matches in some
|
|
892
|
+
// layer — the file-level matches define the visible subset.
|
|
893
|
+
const isSpecificallyHighlighted = hasActiveHighlightLayers
|
|
894
|
+
? layerMatches.some(m =>
|
|
895
|
+
m.item.type === 'file' || !narrowedDirectories.has(m.item.path),
|
|
896
|
+
)
|
|
897
|
+
: true;
|
|
898
|
+
|
|
899
|
+
// Determine collapse/dim/hide behavior based on what's active:
|
|
840
900
|
// - focusDirectory only: collapse if outside focus
|
|
841
901
|
// - highlightLayers only (with collapse mode): collapse if not highlighted
|
|
842
|
-
// -
|
|
902
|
+
// - highlightLayers only (with hide mode): hide if not specifically highlighted
|
|
903
|
+
// - both: collapse if outside focus, dim/hide if in focus but not highlighted
|
|
843
904
|
if (focusDirectory && hasActiveHighlightLayers && isolationMode === 'collapse') {
|
|
844
|
-
// Both active: collapse if outside focus, dim if in focus but not highlighted
|
|
845
905
|
shouldCollapse = !isInFocusDirectory;
|
|
846
906
|
shouldDim = isInFocusDirectory && !isHighlighted;
|
|
907
|
+
} else if (focusDirectory && hasActiveHighlightLayers && isolationMode === 'hide') {
|
|
908
|
+
shouldCollapse = !isInFocusDirectory;
|
|
909
|
+
shouldHide = isInFocusDirectory && !isSpecificallyHighlighted;
|
|
847
910
|
} else if (focusDirectory) {
|
|
848
|
-
// Focus only: collapse if outside focus directory
|
|
849
911
|
shouldCollapse = !isInFocusDirectory;
|
|
850
912
|
} else if (hasActiveHighlightLayers && isolationMode === 'collapse') {
|
|
851
|
-
// Highlight only with collapse: collapse if not highlighted
|
|
852
913
|
shouldCollapse = !isHighlighted;
|
|
914
|
+
} else if (hasActiveHighlightLayers && isolationMode === 'hide') {
|
|
915
|
+
shouldHide = !isSpecificallyHighlighted;
|
|
853
916
|
}
|
|
854
917
|
|
|
855
918
|
// Height: 1.0 = full, 0.05 = flat (collapsed or dimmed)
|
|
@@ -861,8 +924,10 @@ function InstancedBuildings({
|
|
|
861
924
|
// Dim ref controls graying: 0 = gray out, 1 = keep color
|
|
862
925
|
// Collapsed buildings go gray, dimmed buildings keep their color
|
|
863
926
|
targetDimRef.current![index] = shouldCollapse ? 0 : 1;
|
|
927
|
+
// Hidden ref controls full invisibility (mesh + edges + icon)
|
|
928
|
+
hiddenRef.current![index] = shouldHide ? 1 : 0;
|
|
864
929
|
});
|
|
865
|
-
}, [focusDirectory, buildings,
|
|
930
|
+
}, [focusDirectory, buildings, visibilityLayers, isolationMode, hasActiveHighlightLayers, narrowedDirectories]);
|
|
866
931
|
|
|
867
932
|
// Pre-compute building data
|
|
868
933
|
const buildingData = useMemo(() => {
|
|
@@ -962,6 +1027,16 @@ function InstancedBuildings({
|
|
|
962
1027
|
buildingData.forEach((data, instanceIndex) => {
|
|
963
1028
|
const { width, depth, fullHeight, x, z, staggerDelayMs } = data;
|
|
964
1029
|
|
|
1030
|
+
const isHidden = hiddenRef.current?.[instanceIndex] === 1;
|
|
1031
|
+
|
|
1032
|
+
if (isHidden) {
|
|
1033
|
+
tempObject.position.set(x, baseOffset, z);
|
|
1034
|
+
tempObject.scale.set(0, 0, 0);
|
|
1035
|
+
tempObject.updateMatrix();
|
|
1036
|
+
meshRef.current!.setMatrixAt(instanceIndex, tempObject.matrix);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
965
1040
|
// Animate height multiplier towards target
|
|
966
1041
|
const currentMultiplier = heightMultipliersRef.current![instanceIndex];
|
|
967
1042
|
const targetMultiplier = targetMultipliersRef.current![instanceIndex];
|
|
@@ -1089,6 +1164,7 @@ function InstancedBuildings({
|
|
|
1089
1164
|
baseOffset={baseOffset}
|
|
1090
1165
|
springDuration={springDuration}
|
|
1091
1166
|
heightMultipliersRef={heightMultipliersRef}
|
|
1167
|
+
hiddenRef={hiddenRef}
|
|
1092
1168
|
/>
|
|
1093
1169
|
|
|
1094
1170
|
{/* Border highlights (colored, layer-driven) */}
|
|
@@ -1123,6 +1199,8 @@ interface BuildingIconsProps {
|
|
|
1123
1199
|
linearScale: number;
|
|
1124
1200
|
flatPatterns: FlatPattern[];
|
|
1125
1201
|
highlightLayers: HighlightLayer[];
|
|
1202
|
+
/** User-supplied highlight layers only (excludes file-color layers). */
|
|
1203
|
+
visibilityLayers: HighlightLayer[];
|
|
1126
1204
|
isolationMode: IsolationMode;
|
|
1127
1205
|
hasActiveHighlights: boolean;
|
|
1128
1206
|
}
|
|
@@ -1205,9 +1283,34 @@ function BuildingIcons({
|
|
|
1205
1283
|
linearScale,
|
|
1206
1284
|
flatPatterns,
|
|
1207
1285
|
highlightLayers,
|
|
1286
|
+
visibilityLayers,
|
|
1208
1287
|
isolationMode,
|
|
1209
1288
|
hasActiveHighlights,
|
|
1210
1289
|
}: BuildingIconsProps) {
|
|
1290
|
+
// Same narrowing rule as InstancedBuildings, scoped to user highlight layers
|
|
1291
|
+
// only (file-color layers don't narrow visibility).
|
|
1292
|
+
const narrowedDirectories = useMemo(() => {
|
|
1293
|
+
const dirs: string[] = [];
|
|
1294
|
+
const files: string[] = [];
|
|
1295
|
+
for (const layer of visibilityLayers) {
|
|
1296
|
+
if (!layer.enabled) continue;
|
|
1297
|
+
for (const item of layer.items) {
|
|
1298
|
+
if (item.type === 'directory') dirs.push(item.path);
|
|
1299
|
+
else if (item.type === 'file') files.push(item.path);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const narrowed = new Set<string>();
|
|
1303
|
+
for (const dir of dirs) {
|
|
1304
|
+
for (const f of files) {
|
|
1305
|
+
if (f === dir || f.startsWith(dir + '/')) {
|
|
1306
|
+
narrowed.add(dir);
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
return narrowed;
|
|
1312
|
+
}, [visibilityLayers]);
|
|
1313
|
+
|
|
1211
1314
|
// Pre-compute buildings with icons
|
|
1212
1315
|
const buildingsWithIcons = useMemo(() => {
|
|
1213
1316
|
return buildings
|
|
@@ -1215,10 +1318,14 @@ function BuildingIcons({
|
|
|
1215
1318
|
const config = getConfigForFile(building);
|
|
1216
1319
|
if (!config.icon) return null;
|
|
1217
1320
|
|
|
1218
|
-
const
|
|
1219
|
-
const isHighlighted =
|
|
1321
|
+
const matches = getLayerMatchesForPath(building.path, visibilityLayers);
|
|
1322
|
+
const isHighlighted = matches.length > 0;
|
|
1323
|
+
const isSpecificallyHighlighted = matches.some(
|
|
1324
|
+
m => m.item.type === 'file' || !narrowedDirectories.has(m.item.path),
|
|
1325
|
+
);
|
|
1220
1326
|
const shouldDim = hasActiveHighlights && !isHighlighted;
|
|
1221
|
-
const shouldHide =
|
|
1327
|
+
const shouldHide =
|
|
1328
|
+
hasActiveHighlights && isolationMode === 'hide' && !isSpecificallyHighlighted;
|
|
1222
1329
|
const shouldCollapse = shouldDim && isolationMode === 'collapse';
|
|
1223
1330
|
|
|
1224
1331
|
// Hide icons for buildings that are hidden or collapsed
|
|
@@ -1250,12 +1357,13 @@ function BuildingIcons({
|
|
|
1250
1357
|
}, [
|
|
1251
1358
|
buildings,
|
|
1252
1359
|
centerOffset,
|
|
1253
|
-
|
|
1360
|
+
visibilityLayers,
|
|
1254
1361
|
isolationMode,
|
|
1255
1362
|
hasActiveHighlights,
|
|
1256
1363
|
heightScaling,
|
|
1257
1364
|
linearScale,
|
|
1258
1365
|
flatPatterns,
|
|
1366
|
+
narrowedDirectories,
|
|
1259
1367
|
]);
|
|
1260
1368
|
|
|
1261
1369
|
// Icons are now always rendered (flat or grown)
|
|
@@ -1340,14 +1448,6 @@ function DistrictFloor({ district, centerOffset, highlightColor, growProgress }:
|
|
|
1340
1448
|
<lineBasicMaterial color={borderColor} linewidth={lineWidth} depthWrite={false} />
|
|
1341
1449
|
</lineSegments>
|
|
1342
1450
|
|
|
1343
|
-
{/* Highlighted floor fill when focused */}
|
|
1344
|
-
{highlightColor && (
|
|
1345
|
-
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, floorY - 0.1, 0]} renderOrder={-2}>
|
|
1346
|
-
<planeGeometry args={[width, depth]} />
|
|
1347
|
-
<meshBasicMaterial color={highlightColor} transparent opacity={0.15} depthWrite={false} />
|
|
1348
|
-
</mesh>
|
|
1349
|
-
)}
|
|
1350
|
-
|
|
1351
1451
|
{/* Always show directory name label */}
|
|
1352
1452
|
<Text
|
|
1353
1453
|
position={[0, textY, textZ]}
|
|
@@ -1658,7 +1758,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1658
1758
|
// This will be corrected in Frame 1 if aspect is different
|
|
1659
1759
|
const initialHeight = calculateFlatCameraHeight(1);
|
|
1660
1760
|
|
|
1661
|
-
console.log('[Spring init] Initializing with 2D position, height:', initialHeight);
|
|
1662
1761
|
return {
|
|
1663
1762
|
camX: 0,
|
|
1664
1763
|
camY: initialHeight,
|
|
@@ -1668,16 +1767,11 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1668
1767
|
lookZ: 0,
|
|
1669
1768
|
config: { tension: 60, friction: 20 },
|
|
1670
1769
|
onStart: () => {
|
|
1671
|
-
// Only allow animations after initial setup is complete
|
|
1672
1770
|
if (hasAppliedInitial.current) {
|
|
1673
|
-
console.log('[Spring onStart] Animation starting - camY:', camY.get());
|
|
1674
1771
|
isAnimatingRef.current = true;
|
|
1675
|
-
} else {
|
|
1676
|
-
console.log('[Spring onStart] Blocked - initialization not complete');
|
|
1677
1772
|
}
|
|
1678
1773
|
},
|
|
1679
1774
|
onRest: () => {
|
|
1680
|
-
console.log('[Spring onRest] Animation finished');
|
|
1681
1775
|
isAnimatingRef.current = false;
|
|
1682
1776
|
},
|
|
1683
1777
|
};
|
|
@@ -1728,23 +1822,11 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1728
1822
|
// When isFlat changes from true to false, animate to 3D view
|
|
1729
1823
|
// Component always starts in 2D, so we only animate the 2D→3D transition
|
|
1730
1824
|
useEffect(() => {
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
// Skip until camera is initialized
|
|
1734
|
-
if (!hasAppliedInitial.current) {
|
|
1735
|
-
console.log('[useEffect] Skipping - not initialized yet');
|
|
1736
|
-
return;
|
|
1737
|
-
}
|
|
1825
|
+
if (!hasAppliedInitial.current) return;
|
|
1738
1826
|
|
|
1739
|
-
// Only animate if isFlat changed from true to false (2D → 3D transition)
|
|
1740
1827
|
const isFlatChanged = prevIsFlatRef.current !== isFlat;
|
|
1828
|
+
if (!isFlatChanged) return;
|
|
1741
1829
|
|
|
1742
|
-
if (!isFlatChanged) {
|
|
1743
|
-
console.log('[useEffect] No isFlat change - skipping');
|
|
1744
|
-
return;
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
console.log('[useEffect] isFlat changed from', prevIsFlatRef.current, 'to', isFlat, '- animating transition');
|
|
1748
1830
|
prevIsFlatRef.current = isFlat;
|
|
1749
1831
|
|
|
1750
1832
|
// Calculate target position for 3D view
|
|
@@ -1768,7 +1850,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1768
1850
|
targetZ: 0,
|
|
1769
1851
|
};
|
|
1770
1852
|
|
|
1771
|
-
console.log('[api.start#isFlat-toggle]', newPos);
|
|
1772
1853
|
api.start({
|
|
1773
1854
|
camX: newPos.x,
|
|
1774
1855
|
camY: newPos.y,
|
|
@@ -1838,7 +1919,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1838
1919
|
};
|
|
1839
1920
|
}
|
|
1840
1921
|
|
|
1841
|
-
console.log('[api.start#focus-target]', { focusTarget, isFlat, newPos });
|
|
1842
1922
|
api.start({
|
|
1843
1923
|
camX: newPos.x,
|
|
1844
1924
|
camY: newPos.y,
|
|
@@ -1860,14 +1940,12 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1860
1940
|
// Ensure camera FOV is correct (defaults to 75 before prop applies)
|
|
1861
1941
|
const perspCam = camera as THREE.PerspectiveCamera;
|
|
1862
1942
|
if (perspCam.fov !== 50) {
|
|
1863
|
-
console.log('[Frame 1] Correcting FOV from', perspCam.fov, 'to 50');
|
|
1864
1943
|
perspCam.fov = 50;
|
|
1865
1944
|
perspCam.updateProjectionMatrix();
|
|
1866
1945
|
}
|
|
1867
1946
|
|
|
1868
1947
|
// Calculate initial 2D position with correct aspect ratio
|
|
1869
1948
|
const initialPos = getInitial2DPosition();
|
|
1870
|
-
console.log('[Frame 1] Setting camera to initial 2D position:', initialPos);
|
|
1871
1949
|
|
|
1872
1950
|
camera.position.set(initialPos.x, initialPos.y, initialPos.z);
|
|
1873
1951
|
|
|
@@ -1877,7 +1955,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1877
1955
|
controlsRef.current.update();
|
|
1878
1956
|
|
|
1879
1957
|
// Sync spring to match camera position (use immediate to avoid animation)
|
|
1880
|
-
console.log('[api.start#frame1-immediate]', initialPos);
|
|
1881
1958
|
api.start({
|
|
1882
1959
|
camX: initialPos.x,
|
|
1883
1960
|
camY: initialPos.y,
|
|
@@ -1902,32 +1979,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1902
1979
|
// Wait for controls and initialization to complete
|
|
1903
1980
|
if (!controlsRef.current || !hasAppliedInitial.current) return;
|
|
1904
1981
|
|
|
1905
|
-
// [debug] log which branch runs each frame so consumers can identify
|
|
1906
|
-
// unexpected camera mutation when no api.start fired.
|
|
1907
|
-
if (
|
|
1908
|
-
isOrbitingRef.current ||
|
|
1909
|
-
isTiltingRef.current ||
|
|
1910
|
-
isAnimatingRef.current
|
|
1911
|
-
) {
|
|
1912
|
-
const branch = isOrbitingRef.current
|
|
1913
|
-
? 'orbit'
|
|
1914
|
-
: isTiltingRef.current
|
|
1915
|
-
? 'tilt'
|
|
1916
|
-
: 'animating';
|
|
1917
|
-
const w = (
|
|
1918
|
-
globalThis as unknown as { __fileCityFrameLog?: Map<string, number> }
|
|
1919
|
-
).__fileCityFrameLog ??= new Map<string, number>();
|
|
1920
|
-
const last = w.get(branch) ?? 0;
|
|
1921
|
-
const now = performance.now();
|
|
1922
|
-
if (now - last > 200) {
|
|
1923
|
-
w.set(branch, now);
|
|
1924
|
-
// eslint-disable-next-line no-console
|
|
1925
|
-
console.log(`[useFrame#${branch}]`, {
|
|
1926
|
-
camY: camY.get(),
|
|
1927
|
-
posY: camera.position.y,
|
|
1928
|
-
});
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
1982
|
// Handle orbit animation (horizontal rotation along arc)
|
|
1932
1983
|
if (isOrbitingRef.current && orbitParamsRef.current) {
|
|
1933
1984
|
const { centerX, centerZ, distance, height } = orbitParamsRef.current;
|
|
@@ -1994,7 +2045,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1994
2045
|
const targetHeight = citySize * 1.1;
|
|
1995
2046
|
const targetZ = citySize * 1.3;
|
|
1996
2047
|
|
|
1997
|
-
console.log('[api.start#resetToInitial]', { targetHeight, targetZ });
|
|
1998
2048
|
api.start({
|
|
1999
2049
|
camX: 0,
|
|
2000
2050
|
camY: targetHeight,
|
|
@@ -2010,7 +2060,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
2010
2060
|
const distance = Math.max(effectiveSize * 2, 50);
|
|
2011
2061
|
const height = Math.max(effectiveSize * 1.5, 40);
|
|
2012
2062
|
|
|
2013
|
-
console.log('[api.start#moveTo]', { x, z, height, distance });
|
|
2014
2063
|
api.start({
|
|
2015
2064
|
camX: x,
|
|
2016
2065
|
camY: height,
|
|
@@ -2042,7 +2091,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
2042
2091
|
});
|
|
2043
2092
|
}
|
|
2044
2093
|
const config = options?.duration ? { duration: options.duration } : undefined;
|
|
2045
|
-
console.log('[api.start#setFlatView]', { x, z, height, options });
|
|
2046
2094
|
api.start({
|
|
2047
2095
|
camX: x,
|
|
2048
2096
|
camY: height,
|
|
@@ -2077,7 +2125,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
2077
2125
|
? { duration: options.duration, easing: (t: number) => t }
|
|
2078
2126
|
: { tension: 60, friction: 20 };
|
|
2079
2127
|
|
|
2080
|
-
console.log('[api.start#setTarget]', { x, y, z, newCamX, newCamY, newCamZ });
|
|
2081
2128
|
api.start({
|
|
2082
2129
|
camX: newCamX,
|
|
2083
2130
|
camY: newCamY,
|
|
@@ -2818,6 +2865,8 @@ interface CitySceneProps {
|
|
|
2818
2865
|
growProgress: number;
|
|
2819
2866
|
animationConfig: AnimationConfig;
|
|
2820
2867
|
highlightLayers: HighlightLayer[];
|
|
2868
|
+
/** User-supplied highlight layers (no file-color layers) for visibility logic. */
|
|
2869
|
+
visibilityLayers: HighlightLayer[];
|
|
2821
2870
|
isolationMode: IsolationMode;
|
|
2822
2871
|
heightScaling: HeightScaling;
|
|
2823
2872
|
linearScale: number;
|
|
@@ -2843,6 +2892,7 @@ function CityScene({
|
|
|
2843
2892
|
growProgress,
|
|
2844
2893
|
animationConfig,
|
|
2845
2894
|
highlightLayers,
|
|
2895
|
+
visibilityLayers,
|
|
2846
2896
|
isolationMode,
|
|
2847
2897
|
heightScaling,
|
|
2848
2898
|
linearScale,
|
|
@@ -2876,7 +2926,7 @@ function CityScene({
|
|
|
2876
2926
|
return Math.max(...cityData.buildings.map(b => b.dimensions[1]), 0);
|
|
2877
2927
|
}, [adaptCameraToBuildings, cityData.buildings]);
|
|
2878
2928
|
|
|
2879
|
-
const activeHighlights = useMemo(() => hasActiveHighlights(
|
|
2929
|
+
const activeHighlights = useMemo(() => hasActiveHighlights(visibilityLayers), [visibilityLayers]);
|
|
2880
2930
|
|
|
2881
2931
|
// Helper to check if a path is inside a directory
|
|
2882
2932
|
const isPathInDirectory = useCallback((path: string, directory: string) => {
|
|
@@ -3160,6 +3210,7 @@ function CityScene({
|
|
|
3160
3210
|
staggerIndices={staggerIndices}
|
|
3161
3211
|
focusDirectory={buildingFocusDirectory}
|
|
3162
3212
|
highlightLayers={highlightLayers}
|
|
3213
|
+
visibilityLayers={visibilityLayers}
|
|
3163
3214
|
isolationMode={isolationMode}
|
|
3164
3215
|
defaultBuildingColor={defaultBuildingColor}
|
|
3165
3216
|
/>
|
|
@@ -3172,6 +3223,7 @@ function CityScene({
|
|
|
3172
3223
|
linearScale={linearScale}
|
|
3173
3224
|
flatPatterns={flatPatterns}
|
|
3174
3225
|
highlightLayers={highlightLayers}
|
|
3226
|
+
visibilityLayers={visibilityLayers}
|
|
3175
3227
|
isolationMode={isolationMode}
|
|
3176
3228
|
hasActiveHighlights={activeHighlights}
|
|
3177
3229
|
/>
|
|
@@ -3420,6 +3472,13 @@ export function FileCity3D({
|
|
|
3420
3472
|
const focusColor = resolved.focusColor;
|
|
3421
3473
|
const isolationMode = resolved.isolationMode as IsolationMode;
|
|
3422
3474
|
|
|
3475
|
+
// User-supplied highlight layers only — used for visibility decisions so
|
|
3476
|
+
// file-color layers don't keep every building visible in 'hide' mode.
|
|
3477
|
+
const visibilityLayers = useMemo(
|
|
3478
|
+
() => (externalHighlightLayers ?? []) as HighlightLayer[],
|
|
3479
|
+
[externalHighlightLayers],
|
|
3480
|
+
);
|
|
3481
|
+
|
|
3423
3482
|
// `selectedPath` wins over the deprecated `selectedBuilding` when both are
|
|
3424
3483
|
// set. A path resolves to either a building (file selection) or a district
|
|
3425
3484
|
// (directory selection) — never both.
|
|
@@ -3545,6 +3604,7 @@ export function FileCity3D({
|
|
|
3545
3604
|
growProgress={growProgress}
|
|
3546
3605
|
animationConfig={animationConfig}
|
|
3547
3606
|
highlightLayers={highlightLayers}
|
|
3607
|
+
visibilityLayers={visibilityLayers}
|
|
3548
3608
|
isolationMode={isolationMode}
|
|
3549
3609
|
heightScaling={heightScaling}
|
|
3550
3610
|
linearScale={linearScale}
|
|
@@ -392,11 +392,41 @@ const testScenarios: TestScenario[] = [
|
|
|
392
392
|
},
|
|
393
393
|
],
|
|
394
394
|
},
|
|
395
|
+
{
|
|
396
|
+
id: 'S10-dir-plus-files-inside',
|
|
397
|
+
name: 'S10: Directory + Files Inside',
|
|
398
|
+
description:
|
|
399
|
+
'Directory layer on api + file layer for specific routes inside it — directory-only matches inside the dir should be hidden in hide mode',
|
|
400
|
+
focusDirectory: null,
|
|
401
|
+
highlightLayers: [
|
|
402
|
+
{
|
|
403
|
+
id: 'api-dir-layer',
|
|
404
|
+
name: 'API Routes (dir)',
|
|
405
|
+
enabled: true,
|
|
406
|
+
priority: 0,
|
|
407
|
+
color: '#3b82f6',
|
|
408
|
+
items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
id: 'workos-files-layer',
|
|
412
|
+
name: 'WorkOS Routes (files)',
|
|
413
|
+
enabled: true,
|
|
414
|
+
priority: 1,
|
|
415
|
+
color: '#ec4899',
|
|
416
|
+
items: [
|
|
417
|
+
{ path: 'auth-server/src/app/api/auth/workos/callback/route.ts', type: 'file' as const },
|
|
418
|
+
{ path: 'auth-server/src/app/api/auth/workos/verify/route.ts', type: 'file' as const },
|
|
419
|
+
{ path: 'auth-server/src/app/api/auth/workos/token/route.ts', type: 'file' as const },
|
|
420
|
+
],
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
},
|
|
395
424
|
];
|
|
396
425
|
|
|
397
426
|
export const ScenarioComparison: StoryObj = {
|
|
398
427
|
render: function RenderScenarioComparison() {
|
|
399
428
|
const [currentScenarioIndex, setCurrentScenarioIndex] = useState(0);
|
|
429
|
+
const [isGrown, setIsGrown] = useState(true);
|
|
400
430
|
const scenario = testScenarios[currentScenarioIndex];
|
|
401
431
|
const cityData = authServerCityData as CityData;
|
|
402
432
|
|
|
@@ -521,9 +551,11 @@ export const ScenarioComparison: StoryObj = {
|
|
|
521
551
|
fileColorLayers={fileColorLayers}
|
|
522
552
|
focusDirectory={scenario.focusDirectory}
|
|
523
553
|
focusColor={scenario.focusColor}
|
|
554
|
+
isolationMode="hide"
|
|
524
555
|
width="100%"
|
|
525
556
|
height="100%"
|
|
526
|
-
isGrown={
|
|
557
|
+
isGrown={isGrown}
|
|
558
|
+
onGrowChange={setIsGrown}
|
|
527
559
|
showControls={true}
|
|
528
560
|
backgroundColor="#0f1419"
|
|
529
561
|
/>
|