@principal-ai/file-city-react 0.5.50 → 0.5.52
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,39 @@ 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);
|
|
453
462
|
const hasActiveHighlightLayers = useMemo(() => {
|
|
454
|
-
return
|
|
455
|
-
}, [
|
|
463
|
+
return visibilityLayers.some(layer => layer.enabled && layer.items.length > 0);
|
|
464
|
+
}, [visibilityLayers]);
|
|
465
|
+
// Directories matched by a directory-type item from a user-supplied layer
|
|
466
|
+
// that also contain a file-type item from any user-supplied layer. Inside
|
|
467
|
+
// these directories, the directory match alone isn't enough to count as
|
|
468
|
+
// "specifically highlighted" — file-level matches define the visible subset.
|
|
469
|
+
const narrowedDirectories = useMemo(() => {
|
|
470
|
+
const dirs = [];
|
|
471
|
+
const files = [];
|
|
472
|
+
for (const layer of visibilityLayers) {
|
|
473
|
+
if (!layer.enabled)
|
|
474
|
+
continue;
|
|
475
|
+
for (const item of layer.items) {
|
|
476
|
+
if (item.type === 'directory')
|
|
477
|
+
dirs.push(item.path);
|
|
478
|
+
else if (item.type === 'file')
|
|
479
|
+
files.push(item.path);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const narrowed = new Set();
|
|
483
|
+
for (const dir of dirs) {
|
|
484
|
+
for (const f of files) {
|
|
485
|
+
if (f === dir || f.startsWith(dir + '/')) {
|
|
486
|
+
narrowed.add(dir);
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return narrowed;
|
|
492
|
+
}, [visibilityLayers]);
|
|
456
493
|
// Initialize height and dim multiplier arrays
|
|
457
494
|
useEffect(() => {
|
|
458
495
|
if (buildings.length > 0) {
|
|
@@ -462,38 +499,53 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
462
499
|
targetMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
463
500
|
dimMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
464
501
|
targetDimRef.current = new Float32Array(buildings.length).fill(1);
|
|
502
|
+
hiddenRef.current = new Uint8Array(buildings.length);
|
|
465
503
|
}
|
|
466
504
|
}
|
|
467
505
|
}, [buildings.length]);
|
|
468
506
|
// Update target multipliers when focusDirectory or highlightLayers change
|
|
469
507
|
useEffect(() => {
|
|
470
|
-
if (!targetMultipliersRef.current || !targetDimRef.current)
|
|
508
|
+
if (!targetMultipliersRef.current || !targetDimRef.current || !hiddenRef.current)
|
|
471
509
|
return;
|
|
472
510
|
buildings.forEach((building, index) => {
|
|
473
511
|
let shouldCollapse = false;
|
|
474
512
|
let shouldDim = false;
|
|
513
|
+
let shouldHide = false;
|
|
475
514
|
const isInFocusDirectory = focusDirectory
|
|
476
515
|
? isPathInDirectory(building.path, focusDirectory)
|
|
477
516
|
: true; // No focusDirectory means all are "in focus"
|
|
517
|
+
const layerMatches = hasActiveHighlightLayers
|
|
518
|
+
? getLayerMatchesForPath(building.path, visibilityLayers)
|
|
519
|
+
: [];
|
|
478
520
|
const isHighlighted = hasActiveHighlightLayers
|
|
479
|
-
?
|
|
521
|
+
? layerMatches.length > 0
|
|
480
522
|
: true; // No highlights means all are "highlighted"
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
// -
|
|
484
|
-
|
|
523
|
+
// A directory match doesn't count as "specifically highlighted" when
|
|
524
|
+
// that directory has been narrowed by file-level matches — the
|
|
525
|
+
// file-level matches define the visible subset within it.
|
|
526
|
+
const isSpecificallyHighlighted = hasActiveHighlightLayers
|
|
527
|
+
? layerMatches.some(m => m.item.type === 'file' || !narrowedDirectories.has(m.item.path))
|
|
528
|
+
: true;
|
|
529
|
+
// Determine collapse/dim/hide behavior based on what's active. The
|
|
530
|
+
// "specifically highlighted" check applies in both collapse and hide
|
|
531
|
+
// modes so directory matches narrowed by file-level matches don't keep
|
|
532
|
+
// every sibling visible.
|
|
485
533
|
if (focusDirectory && hasActiveHighlightLayers && isolationMode === 'collapse') {
|
|
486
|
-
// Both active: collapse if outside focus, dim if in focus but not highlighted
|
|
487
534
|
shouldCollapse = !isInFocusDirectory;
|
|
488
|
-
shouldDim = isInFocusDirectory && !
|
|
535
|
+
shouldDim = isInFocusDirectory && !isSpecificallyHighlighted;
|
|
536
|
+
}
|
|
537
|
+
else if (focusDirectory && hasActiveHighlightLayers && isolationMode === 'hide') {
|
|
538
|
+
shouldCollapse = !isInFocusDirectory;
|
|
539
|
+
shouldHide = isInFocusDirectory && !isSpecificallyHighlighted;
|
|
489
540
|
}
|
|
490
541
|
else if (focusDirectory) {
|
|
491
|
-
// Focus only: collapse if outside focus directory
|
|
492
542
|
shouldCollapse = !isInFocusDirectory;
|
|
493
543
|
}
|
|
494
544
|
else if (hasActiveHighlightLayers && isolationMode === 'collapse') {
|
|
495
|
-
|
|
496
|
-
|
|
545
|
+
shouldCollapse = !isSpecificallyHighlighted;
|
|
546
|
+
}
|
|
547
|
+
else if (hasActiveHighlightLayers && isolationMode === 'hide') {
|
|
548
|
+
shouldHide = !isSpecificallyHighlighted;
|
|
497
549
|
}
|
|
498
550
|
// Height: 1.0 = full, 0.05 = flat (collapsed or dimmed)
|
|
499
551
|
if (shouldCollapse || shouldDim) {
|
|
@@ -505,8 +557,10 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
505
557
|
// Dim ref controls graying: 0 = gray out, 1 = keep color
|
|
506
558
|
// Collapsed buildings go gray, dimmed buildings keep their color
|
|
507
559
|
targetDimRef.current[index] = shouldCollapse ? 0 : 1;
|
|
560
|
+
// Hidden ref controls full invisibility (mesh + edges + icon)
|
|
561
|
+
hiddenRef.current[index] = shouldHide ? 1 : 0;
|
|
508
562
|
});
|
|
509
|
-
}, [focusDirectory, buildings,
|
|
563
|
+
}, [focusDirectory, buildings, visibilityLayers, isolationMode, hasActiveHighlightLayers, narrowedDirectories]);
|
|
510
564
|
// Pre-compute building data
|
|
511
565
|
const buildingData = useMemo(() => {
|
|
512
566
|
return buildings.map((building, index) => {
|
|
@@ -588,6 +642,14 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
588
642
|
const collapseSpeed = 0.08;
|
|
589
643
|
buildingData.forEach((data, instanceIndex) => {
|
|
590
644
|
const { width, depth, fullHeight, x, z, staggerDelayMs } = data;
|
|
645
|
+
const isHidden = hiddenRef.current?.[instanceIndex] === 1;
|
|
646
|
+
if (isHidden) {
|
|
647
|
+
tempObject.position.set(x, baseOffset, z);
|
|
648
|
+
tempObject.scale.set(0, 0, 0);
|
|
649
|
+
tempObject.updateMatrix();
|
|
650
|
+
meshRef.current.setMatrixAt(instanceIndex, tempObject.matrix);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
591
653
|
// Animate height multiplier towards target
|
|
592
654
|
const currentMultiplier = heightMultipliersRef.current[instanceIndex];
|
|
593
655
|
const targetMultiplier = targetMultipliersRef.current[instanceIndex];
|
|
@@ -671,7 +733,7 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
671
733
|
z: d.z,
|
|
672
734
|
staggerDelayMs: d.staggerDelayMs,
|
|
673
735
|
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 })] }));
|
|
736
|
+
})), 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
737
|
}
|
|
676
738
|
function AnimatedIcon({ x, z, targetHeight, iconSize, texture, opacity, growProgress, }) {
|
|
677
739
|
const meshRef = useRef(null);
|
|
@@ -699,7 +761,33 @@ function AnimatedIcon({ x, z, targetHeight, iconSize, texture, opacity, growProg
|
|
|
699
761
|
});
|
|
700
762
|
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
763
|
}
|
|
702
|
-
function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, linearScale, flatPatterns, highlightLayers, isolationMode, hasActiveHighlights, }) {
|
|
764
|
+
function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, linearScale, flatPatterns, highlightLayers, visibilityLayers, isolationMode, hasActiveHighlights, }) {
|
|
765
|
+
// Same narrowing rule as InstancedBuildings, scoped to user highlight layers
|
|
766
|
+
// only (file-color layers don't narrow visibility).
|
|
767
|
+
const narrowedDirectories = useMemo(() => {
|
|
768
|
+
const dirs = [];
|
|
769
|
+
const files = [];
|
|
770
|
+
for (const layer of visibilityLayers) {
|
|
771
|
+
if (!layer.enabled)
|
|
772
|
+
continue;
|
|
773
|
+
for (const item of layer.items) {
|
|
774
|
+
if (item.type === 'directory')
|
|
775
|
+
dirs.push(item.path);
|
|
776
|
+
else if (item.type === 'file')
|
|
777
|
+
files.push(item.path);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const narrowed = new Set();
|
|
781
|
+
for (const dir of dirs) {
|
|
782
|
+
for (const f of files) {
|
|
783
|
+
if (f === dir || f.startsWith(dir + '/')) {
|
|
784
|
+
narrowed.add(dir);
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return narrowed;
|
|
790
|
+
}, [visibilityLayers]);
|
|
703
791
|
// Pre-compute buildings with icons
|
|
704
792
|
const buildingsWithIcons = useMemo(() => {
|
|
705
793
|
return buildings
|
|
@@ -707,10 +795,11 @@ function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, l
|
|
|
707
795
|
const config = getConfigForFile(building);
|
|
708
796
|
if (!config.icon)
|
|
709
797
|
return null;
|
|
710
|
-
const
|
|
711
|
-
const isHighlighted =
|
|
712
|
-
const
|
|
713
|
-
const
|
|
798
|
+
const matches = getLayerMatchesForPath(building.path, visibilityLayers);
|
|
799
|
+
const isHighlighted = matches.length > 0;
|
|
800
|
+
const isSpecificallyHighlighted = matches.some(m => m.item.type === 'file' || !narrowedDirectories.has(m.item.path));
|
|
801
|
+
const shouldDim = hasActiveHighlights && !isSpecificallyHighlighted;
|
|
802
|
+
const shouldHide = hasActiveHighlights && isolationMode === 'hide' && !isSpecificallyHighlighted;
|
|
714
803
|
const shouldCollapse = shouldDim && isolationMode === 'collapse';
|
|
715
804
|
// Hide icons for buildings that are hidden or collapsed
|
|
716
805
|
if (shouldHide || shouldCollapse)
|
|
@@ -732,12 +821,13 @@ function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, l
|
|
|
732
821
|
}, [
|
|
733
822
|
buildings,
|
|
734
823
|
centerOffset,
|
|
735
|
-
|
|
824
|
+
visibilityLayers,
|
|
736
825
|
isolationMode,
|
|
737
826
|
hasActiveHighlights,
|
|
738
827
|
heightScaling,
|
|
739
828
|
linearScale,
|
|
740
829
|
flatPatterns,
|
|
830
|
+
narrowedDirectories,
|
|
741
831
|
]);
|
|
742
832
|
// Icons are now always rendered (flat or grown)
|
|
743
833
|
return (_jsx(_Fragment, { children: buildingsWithIcons.map(({ building, config, x, z, targetHeight, shouldDim }) => {
|
|
@@ -777,7 +867,7 @@ function DistrictFloor({ district, centerOffset, highlightColor, growProgress })
|
|
|
777
867
|
const flatZ = depth / 2 - 6; // Near bottom of district when flat, with padding
|
|
778
868
|
const grownZ = depth / 2 + 2; // Just outside edge when grown
|
|
779
869
|
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 })] }),
|
|
870
|
+
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
871
|
}
|
|
782
872
|
export const DEFAULT_CAMERA_CONTROLS = {
|
|
783
873
|
leftDrag: 'pan',
|
|
@@ -957,7 +1047,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
957
1047
|
// Calculate initial position with default aspect ratio
|
|
958
1048
|
// This will be corrected in Frame 1 if aspect is different
|
|
959
1049
|
const initialHeight = calculateFlatCameraHeight(1);
|
|
960
|
-
console.log('[Spring init] Initializing with 2D position, height:', initialHeight);
|
|
961
1050
|
return {
|
|
962
1051
|
camX: 0,
|
|
963
1052
|
camY: initialHeight,
|
|
@@ -967,17 +1056,11 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
967
1056
|
lookZ: 0,
|
|
968
1057
|
config: { tension: 60, friction: 20 },
|
|
969
1058
|
onStart: () => {
|
|
970
|
-
// Only allow animations after initial setup is complete
|
|
971
1059
|
if (hasAppliedInitial.current) {
|
|
972
|
-
console.log('[Spring onStart] Animation starting - camY:', camY.get());
|
|
973
1060
|
isAnimatingRef.current = true;
|
|
974
1061
|
}
|
|
975
|
-
else {
|
|
976
|
-
console.log('[Spring onStart] Blocked - initialization not complete');
|
|
977
|
-
}
|
|
978
1062
|
},
|
|
979
1063
|
onRest: () => {
|
|
980
|
-
console.log('[Spring onRest] Animation finished');
|
|
981
1064
|
isAnimatingRef.current = false;
|
|
982
1065
|
},
|
|
983
1066
|
};
|
|
@@ -1012,19 +1095,11 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1012
1095
|
// When isFlat changes from true to false, animate to 3D view
|
|
1013
1096
|
// Component always starts in 2D, so we only animate the 2D→3D transition
|
|
1014
1097
|
useEffect(() => {
|
|
1015
|
-
|
|
1016
|
-
// Skip until camera is initialized
|
|
1017
|
-
if (!hasAppliedInitial.current) {
|
|
1018
|
-
console.log('[useEffect] Skipping - not initialized yet');
|
|
1098
|
+
if (!hasAppliedInitial.current)
|
|
1019
1099
|
return;
|
|
1020
|
-
}
|
|
1021
|
-
// Only animate if isFlat changed from true to false (2D → 3D transition)
|
|
1022
1100
|
const isFlatChanged = prevIsFlatRef.current !== isFlat;
|
|
1023
|
-
if (!isFlatChanged)
|
|
1024
|
-
console.log('[useEffect] No isFlat change - skipping');
|
|
1101
|
+
if (!isFlatChanged)
|
|
1025
1102
|
return;
|
|
1026
|
-
}
|
|
1027
|
-
console.log('[useEffect] isFlat changed from', prevIsFlatRef.current, 'to', isFlat, '- animating transition');
|
|
1028
1103
|
prevIsFlatRef.current = isFlat;
|
|
1029
1104
|
// Calculate target position for 3D view
|
|
1030
1105
|
const newPos = isFlat
|
|
@@ -1046,7 +1121,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1046
1121
|
targetY: 0,
|
|
1047
1122
|
targetZ: 0,
|
|
1048
1123
|
};
|
|
1049
|
-
console.log('[api.start#isFlat-toggle]', newPos);
|
|
1050
1124
|
api.start({
|
|
1051
1125
|
camX: newPos.x,
|
|
1052
1126
|
camY: newPos.y,
|
|
@@ -1109,7 +1183,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1109
1183
|
targetZ: 0,
|
|
1110
1184
|
};
|
|
1111
1185
|
}
|
|
1112
|
-
console.log('[api.start#focus-target]', { focusTarget, isFlat, newPos });
|
|
1113
1186
|
api.start({
|
|
1114
1187
|
camX: newPos.x,
|
|
1115
1188
|
camY: newPos.y,
|
|
@@ -1129,20 +1202,17 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1129
1202
|
// Ensure camera FOV is correct (defaults to 75 before prop applies)
|
|
1130
1203
|
const perspCam = camera;
|
|
1131
1204
|
if (perspCam.fov !== 50) {
|
|
1132
|
-
console.log('[Frame 1] Correcting FOV from', perspCam.fov, 'to 50');
|
|
1133
1205
|
perspCam.fov = 50;
|
|
1134
1206
|
perspCam.updateProjectionMatrix();
|
|
1135
1207
|
}
|
|
1136
1208
|
// Calculate initial 2D position with correct aspect ratio
|
|
1137
1209
|
const initialPos = getInitial2DPosition();
|
|
1138
|
-
console.log('[Frame 1] Setting camera to initial 2D position:', initialPos);
|
|
1139
1210
|
camera.position.set(initialPos.x, initialPos.y, initialPos.z);
|
|
1140
1211
|
// Wait for controls to be ready, then set target and sync spring
|
|
1141
1212
|
if (controlsRef.current) {
|
|
1142
1213
|
controlsRef.current.target.set(initialPos.targetX, initialPos.targetY, initialPos.targetZ);
|
|
1143
1214
|
controlsRef.current.update();
|
|
1144
1215
|
// Sync spring to match camera position (use immediate to avoid animation)
|
|
1145
|
-
console.log('[api.start#frame1-immediate]', initialPos);
|
|
1146
1216
|
api.start({
|
|
1147
1217
|
camX: initialPos.x,
|
|
1148
1218
|
camY: initialPos.y,
|
|
@@ -1164,35 +1234,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1164
1234
|
// Wait for controls and initialization to complete
|
|
1165
1235
|
if (!controlsRef.current || !hasAppliedInitial.current)
|
|
1166
1236
|
return;
|
|
1167
|
-
// [debug] log every frame: which branch is active + spring vs camera
|
|
1168
|
-
// position. Throttled to every 250ms. Catches both spring-driven motion
|
|
1169
|
-
// AND external mutation (when no branch is active but position changes).
|
|
1170
|
-
{
|
|
1171
|
-
const w = globalThis;
|
|
1172
|
-
w.__fileCityFrameLog ?? (w.__fileCityFrameLog = { last: 0, lastY: NaN });
|
|
1173
|
-
const log = w.__fileCityFrameLog;
|
|
1174
|
-
const now = performance.now();
|
|
1175
|
-
const yChanged = Math.abs(camera.position.y - log.lastY) > 0.5 ||
|
|
1176
|
-
Number.isNaN(log.lastY);
|
|
1177
|
-
if (now - log.last > 250 && yChanged) {
|
|
1178
|
-
log.last = now;
|
|
1179
|
-
log.lastY = camera.position.y;
|
|
1180
|
-
const branch = isOrbitingRef.current
|
|
1181
|
-
? 'orbit'
|
|
1182
|
-
: isTiltingRef.current
|
|
1183
|
-
? 'tilt'
|
|
1184
|
-
: isAnimatingRef.current
|
|
1185
|
-
? 'animating'
|
|
1186
|
-
: 'idle';
|
|
1187
|
-
// eslint-disable-next-line no-console
|
|
1188
|
-
console.log(`[useFrame#${branch}]`, {
|
|
1189
|
-
springY: camY.get(),
|
|
1190
|
-
posY: camera.position.y,
|
|
1191
|
-
springZ: camZ.get(),
|
|
1192
|
-
posZ: camera.position.z,
|
|
1193
|
-
});
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
1237
|
// Handle orbit animation (horizontal rotation along arc)
|
|
1197
1238
|
if (isOrbitingRef.current && orbitParamsRef.current) {
|
|
1198
1239
|
const { centerX, centerZ, distance, height } = orbitParamsRef.current;
|
|
@@ -1250,7 +1291,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1250
1291
|
const resetToInitial = useCallback(() => {
|
|
1251
1292
|
const targetHeight = citySize * 1.1;
|
|
1252
1293
|
const targetZ = citySize * 1.3;
|
|
1253
|
-
console.log('[api.start#resetToInitial]', { targetHeight, targetZ });
|
|
1254
1294
|
api.start({
|
|
1255
1295
|
camX: 0,
|
|
1256
1296
|
camY: targetHeight,
|
|
@@ -1264,7 +1304,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1264
1304
|
const effectiveSize = size ?? citySize * 0.3;
|
|
1265
1305
|
const distance = Math.max(effectiveSize * 2, 50);
|
|
1266
1306
|
const height = Math.max(effectiveSize * 1.5, 40);
|
|
1267
|
-
console.log('[api.start#moveTo]', { x, z, height, distance });
|
|
1268
1307
|
api.start({
|
|
1269
1308
|
camX: x,
|
|
1270
1309
|
camY: height,
|
|
@@ -1294,7 +1333,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1294
1333
|
});
|
|
1295
1334
|
}
|
|
1296
1335
|
const config = options?.duration ? { duration: options.duration } : undefined;
|
|
1297
|
-
console.log('[api.start#setFlatView]', { x, z, height, options });
|
|
1298
1336
|
api.start({
|
|
1299
1337
|
camX: x,
|
|
1300
1338
|
camY: height,
|
|
@@ -1322,7 +1360,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1322
1360
|
const animConfig = options?.duration
|
|
1323
1361
|
? { duration: options.duration, easing: (t) => t }
|
|
1324
1362
|
: { tension: 60, friction: 20 };
|
|
1325
|
-
console.log('[api.start#setTarget]', { x, y, z, newCamX, newCamY, newCamZ });
|
|
1326
1363
|
api.start({
|
|
1327
1364
|
camX: newCamX,
|
|
1328
1365
|
camY: newCamY,
|
|
@@ -1770,7 +1807,7 @@ function SelectionRing({ district, centerOffset, color, borderWidth, growProgres
|
|
|
1770
1807
|
const barH = 0.5;
|
|
1771
1808
|
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 })] })] }));
|
|
1772
1809
|
}
|
|
1773
|
-
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, }) {
|
|
1810
|
+
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, }) {
|
|
1774
1811
|
const centerOffset = useMemo(() => ({
|
|
1775
1812
|
x: (cityData.bounds.minX + cityData.bounds.maxX) / 2,
|
|
1776
1813
|
z: (cityData.bounds.minZ + cityData.bounds.maxZ) / 2,
|
|
@@ -1782,7 +1819,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
1782
1819
|
return 0;
|
|
1783
1820
|
return Math.max(...cityData.buildings.map(b => b.dimensions[1]), 0);
|
|
1784
1821
|
}, [adaptCameraToBuildings, cityData.buildings]);
|
|
1785
|
-
const activeHighlights = useMemo(() => hasActiveHighlights(
|
|
1822
|
+
const activeHighlights = useMemo(() => hasActiveHighlights(visibilityLayers), [visibilityLayers]);
|
|
1786
1823
|
// Helper to check if a path is inside a directory
|
|
1787
1824
|
const isPathInDirectory = useCallback((path, directory) => {
|
|
1788
1825
|
if (!directory)
|
|
@@ -1974,7 +2011,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
1974
2011
|
// Focus color takes priority, then highlight layer color
|
|
1975
2012
|
const districtColor = (isFocused && buildingFocusColor) ? buildingFocusColor : highlightLayerColor;
|
|
1976
2013
|
return (_jsx(DistrictFloor, { district: district, centerOffset: centerOffset, opacity: 1, highlightColor: districtColor, growProgress: growProgress }, district.path));
|
|
1977
|
-
}), _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 &&
|
|
2014
|
+
}), _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 &&
|
|
1978
2015
|
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 }))] }));
|
|
1979
2016
|
}
|
|
1980
2017
|
/**
|
|
@@ -2024,6 +2061,9 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
2024
2061
|
const focusDirectory = resolved.focusDirectory;
|
|
2025
2062
|
const focusColor = resolved.focusColor;
|
|
2026
2063
|
const isolationMode = resolved.isolationMode;
|
|
2064
|
+
// User-supplied highlight layers only — used for visibility decisions so
|
|
2065
|
+
// file-color layers don't keep every building visible in 'hide' mode.
|
|
2066
|
+
const visibilityLayers = useMemo(() => (externalHighlightLayers ?? []), [externalHighlightLayers]);
|
|
2027
2067
|
// `selectedPath` wins over the deprecated `selectedBuilding` when both are
|
|
2028
2068
|
// set. A path resolves to either a building (file selection) or a district
|
|
2029
2069
|
// (directory selection) — never both.
|
|
@@ -2107,6 +2147,6 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
2107
2147
|
height: '100%',
|
|
2108
2148
|
opacity: cameraReady ? 1 : 0,
|
|
2109
2149
|
transition: 'opacity 0.1s ease-in',
|
|
2110
|
-
}, 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) }))] }));
|
|
2150
|
+
}, 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) }))] }));
|
|
2111
2151
|
}
|
|
2112
2152
|
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,38 @@ 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
|
|
804
821
|
const hasActiveHighlightLayers = useMemo(() => {
|
|
805
|
-
return
|
|
806
|
-
}, [
|
|
822
|
+
return visibilityLayers.some(layer => layer.enabled && layer.items.length > 0);
|
|
823
|
+
}, [visibilityLayers]);
|
|
824
|
+
|
|
825
|
+
// Directories matched by a directory-type item from a user-supplied layer
|
|
826
|
+
// that also contain a file-type item from any user-supplied layer. Inside
|
|
827
|
+
// these directories, the directory match alone isn't enough to count as
|
|
828
|
+
// "specifically highlighted" — file-level matches define the visible subset.
|
|
829
|
+
const narrowedDirectories = useMemo(() => {
|
|
830
|
+
const dirs: string[] = [];
|
|
831
|
+
const files: string[] = [];
|
|
832
|
+
for (const layer of visibilityLayers) {
|
|
833
|
+
if (!layer.enabled) continue;
|
|
834
|
+
for (const item of layer.items) {
|
|
835
|
+
if (item.type === 'directory') dirs.push(item.path);
|
|
836
|
+
else if (item.type === 'file') files.push(item.path);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const narrowed = new Set<string>();
|
|
840
|
+
for (const dir of dirs) {
|
|
841
|
+
for (const f of files) {
|
|
842
|
+
if (f === dir || f.startsWith(dir + '/')) {
|
|
843
|
+
narrowed.add(dir);
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return narrowed;
|
|
849
|
+
}, [visibilityLayers]);
|
|
807
850
|
|
|
808
851
|
// Initialize height and dim multiplier arrays
|
|
809
852
|
useEffect(() => {
|
|
@@ -816,40 +859,56 @@ function InstancedBuildings({
|
|
|
816
859
|
targetMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
817
860
|
dimMultipliersRef.current = new Float32Array(buildings.length).fill(1);
|
|
818
861
|
targetDimRef.current = new Float32Array(buildings.length).fill(1);
|
|
862
|
+
hiddenRef.current = new Uint8Array(buildings.length);
|
|
819
863
|
}
|
|
820
864
|
}
|
|
821
865
|
}, [buildings.length]);
|
|
822
866
|
|
|
823
867
|
// Update target multipliers when focusDirectory or highlightLayers change
|
|
824
868
|
useEffect(() => {
|
|
825
|
-
if (!targetMultipliersRef.current || !targetDimRef.current) return;
|
|
869
|
+
if (!targetMultipliersRef.current || !targetDimRef.current || !hiddenRef.current) return;
|
|
826
870
|
|
|
827
871
|
buildings.forEach((building, index) => {
|
|
828
872
|
let shouldCollapse = false;
|
|
829
873
|
let shouldDim = false;
|
|
874
|
+
let shouldHide = false;
|
|
830
875
|
|
|
831
876
|
const isInFocusDirectory = focusDirectory
|
|
832
877
|
? isPathInDirectory(building.path, focusDirectory)
|
|
833
878
|
: true; // No focusDirectory means all are "in focus"
|
|
834
879
|
|
|
880
|
+
const layerMatches = hasActiveHighlightLayers
|
|
881
|
+
? getLayerMatchesForPath(building.path, visibilityLayers)
|
|
882
|
+
: [];
|
|
835
883
|
const isHighlighted = hasActiveHighlightLayers
|
|
836
|
-
?
|
|
884
|
+
? layerMatches.length > 0
|
|
837
885
|
: true; // No highlights means all are "highlighted"
|
|
838
886
|
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
// -
|
|
842
|
-
|
|
887
|
+
// A directory match doesn't count as "specifically highlighted" when
|
|
888
|
+
// that directory has been narrowed by file-level matches — the
|
|
889
|
+
// file-level matches define the visible subset within it.
|
|
890
|
+
const isSpecificallyHighlighted = hasActiveHighlightLayers
|
|
891
|
+
? layerMatches.some(m =>
|
|
892
|
+
m.item.type === 'file' || !narrowedDirectories.has(m.item.path),
|
|
893
|
+
)
|
|
894
|
+
: true;
|
|
895
|
+
|
|
896
|
+
// Determine collapse/dim/hide behavior based on what's active. The
|
|
897
|
+
// "specifically highlighted" check applies in both collapse and hide
|
|
898
|
+
// modes so directory matches narrowed by file-level matches don't keep
|
|
899
|
+
// every sibling visible.
|
|
843
900
|
if (focusDirectory && hasActiveHighlightLayers && isolationMode === 'collapse') {
|
|
844
|
-
// Both active: collapse if outside focus, dim if in focus but not highlighted
|
|
845
901
|
shouldCollapse = !isInFocusDirectory;
|
|
846
|
-
shouldDim = isInFocusDirectory && !
|
|
902
|
+
shouldDim = isInFocusDirectory && !isSpecificallyHighlighted;
|
|
903
|
+
} else if (focusDirectory && hasActiveHighlightLayers && isolationMode === 'hide') {
|
|
904
|
+
shouldCollapse = !isInFocusDirectory;
|
|
905
|
+
shouldHide = isInFocusDirectory && !isSpecificallyHighlighted;
|
|
847
906
|
} else if (focusDirectory) {
|
|
848
|
-
// Focus only: collapse if outside focus directory
|
|
849
907
|
shouldCollapse = !isInFocusDirectory;
|
|
850
908
|
} else if (hasActiveHighlightLayers && isolationMode === 'collapse') {
|
|
851
|
-
|
|
852
|
-
|
|
909
|
+
shouldCollapse = !isSpecificallyHighlighted;
|
|
910
|
+
} else if (hasActiveHighlightLayers && isolationMode === 'hide') {
|
|
911
|
+
shouldHide = !isSpecificallyHighlighted;
|
|
853
912
|
}
|
|
854
913
|
|
|
855
914
|
// Height: 1.0 = full, 0.05 = flat (collapsed or dimmed)
|
|
@@ -861,8 +920,10 @@ function InstancedBuildings({
|
|
|
861
920
|
// Dim ref controls graying: 0 = gray out, 1 = keep color
|
|
862
921
|
// Collapsed buildings go gray, dimmed buildings keep their color
|
|
863
922
|
targetDimRef.current![index] = shouldCollapse ? 0 : 1;
|
|
923
|
+
// Hidden ref controls full invisibility (mesh + edges + icon)
|
|
924
|
+
hiddenRef.current![index] = shouldHide ? 1 : 0;
|
|
864
925
|
});
|
|
865
|
-
}, [focusDirectory, buildings,
|
|
926
|
+
}, [focusDirectory, buildings, visibilityLayers, isolationMode, hasActiveHighlightLayers, narrowedDirectories]);
|
|
866
927
|
|
|
867
928
|
// Pre-compute building data
|
|
868
929
|
const buildingData = useMemo(() => {
|
|
@@ -962,6 +1023,16 @@ function InstancedBuildings({
|
|
|
962
1023
|
buildingData.forEach((data, instanceIndex) => {
|
|
963
1024
|
const { width, depth, fullHeight, x, z, staggerDelayMs } = data;
|
|
964
1025
|
|
|
1026
|
+
const isHidden = hiddenRef.current?.[instanceIndex] === 1;
|
|
1027
|
+
|
|
1028
|
+
if (isHidden) {
|
|
1029
|
+
tempObject.position.set(x, baseOffset, z);
|
|
1030
|
+
tempObject.scale.set(0, 0, 0);
|
|
1031
|
+
tempObject.updateMatrix();
|
|
1032
|
+
meshRef.current!.setMatrixAt(instanceIndex, tempObject.matrix);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
965
1036
|
// Animate height multiplier towards target
|
|
966
1037
|
const currentMultiplier = heightMultipliersRef.current![instanceIndex];
|
|
967
1038
|
const targetMultiplier = targetMultipliersRef.current![instanceIndex];
|
|
@@ -1089,6 +1160,7 @@ function InstancedBuildings({
|
|
|
1089
1160
|
baseOffset={baseOffset}
|
|
1090
1161
|
springDuration={springDuration}
|
|
1091
1162
|
heightMultipliersRef={heightMultipliersRef}
|
|
1163
|
+
hiddenRef={hiddenRef}
|
|
1092
1164
|
/>
|
|
1093
1165
|
|
|
1094
1166
|
{/* Border highlights (colored, layer-driven) */}
|
|
@@ -1123,6 +1195,8 @@ interface BuildingIconsProps {
|
|
|
1123
1195
|
linearScale: number;
|
|
1124
1196
|
flatPatterns: FlatPattern[];
|
|
1125
1197
|
highlightLayers: HighlightLayer[];
|
|
1198
|
+
/** User-supplied highlight layers only (excludes file-color layers). */
|
|
1199
|
+
visibilityLayers: HighlightLayer[];
|
|
1126
1200
|
isolationMode: IsolationMode;
|
|
1127
1201
|
hasActiveHighlights: boolean;
|
|
1128
1202
|
}
|
|
@@ -1205,9 +1279,34 @@ function BuildingIcons({
|
|
|
1205
1279
|
linearScale,
|
|
1206
1280
|
flatPatterns,
|
|
1207
1281
|
highlightLayers,
|
|
1282
|
+
visibilityLayers,
|
|
1208
1283
|
isolationMode,
|
|
1209
1284
|
hasActiveHighlights,
|
|
1210
1285
|
}: BuildingIconsProps) {
|
|
1286
|
+
// Same narrowing rule as InstancedBuildings, scoped to user highlight layers
|
|
1287
|
+
// only (file-color layers don't narrow visibility).
|
|
1288
|
+
const narrowedDirectories = useMemo(() => {
|
|
1289
|
+
const dirs: string[] = [];
|
|
1290
|
+
const files: string[] = [];
|
|
1291
|
+
for (const layer of visibilityLayers) {
|
|
1292
|
+
if (!layer.enabled) continue;
|
|
1293
|
+
for (const item of layer.items) {
|
|
1294
|
+
if (item.type === 'directory') dirs.push(item.path);
|
|
1295
|
+
else if (item.type === 'file') files.push(item.path);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
const narrowed = new Set<string>();
|
|
1299
|
+
for (const dir of dirs) {
|
|
1300
|
+
for (const f of files) {
|
|
1301
|
+
if (f === dir || f.startsWith(dir + '/')) {
|
|
1302
|
+
narrowed.add(dir);
|
|
1303
|
+
break;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return narrowed;
|
|
1308
|
+
}, [visibilityLayers]);
|
|
1309
|
+
|
|
1211
1310
|
// Pre-compute buildings with icons
|
|
1212
1311
|
const buildingsWithIcons = useMemo(() => {
|
|
1213
1312
|
return buildings
|
|
@@ -1215,10 +1314,14 @@ function BuildingIcons({
|
|
|
1215
1314
|
const config = getConfigForFile(building);
|
|
1216
1315
|
if (!config.icon) return null;
|
|
1217
1316
|
|
|
1218
|
-
const
|
|
1219
|
-
const isHighlighted =
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1317
|
+
const matches = getLayerMatchesForPath(building.path, visibilityLayers);
|
|
1318
|
+
const isHighlighted = matches.length > 0;
|
|
1319
|
+
const isSpecificallyHighlighted = matches.some(
|
|
1320
|
+
m => m.item.type === 'file' || !narrowedDirectories.has(m.item.path),
|
|
1321
|
+
);
|
|
1322
|
+
const shouldDim = hasActiveHighlights && !isSpecificallyHighlighted;
|
|
1323
|
+
const shouldHide =
|
|
1324
|
+
hasActiveHighlights && isolationMode === 'hide' && !isSpecificallyHighlighted;
|
|
1222
1325
|
const shouldCollapse = shouldDim && isolationMode === 'collapse';
|
|
1223
1326
|
|
|
1224
1327
|
// Hide icons for buildings that are hidden or collapsed
|
|
@@ -1250,12 +1353,13 @@ function BuildingIcons({
|
|
|
1250
1353
|
}, [
|
|
1251
1354
|
buildings,
|
|
1252
1355
|
centerOffset,
|
|
1253
|
-
|
|
1356
|
+
visibilityLayers,
|
|
1254
1357
|
isolationMode,
|
|
1255
1358
|
hasActiveHighlights,
|
|
1256
1359
|
heightScaling,
|
|
1257
1360
|
linearScale,
|
|
1258
1361
|
flatPatterns,
|
|
1362
|
+
narrowedDirectories,
|
|
1259
1363
|
]);
|
|
1260
1364
|
|
|
1261
1365
|
// Icons are now always rendered (flat or grown)
|
|
@@ -1340,14 +1444,6 @@ function DistrictFloor({ district, centerOffset, highlightColor, growProgress }:
|
|
|
1340
1444
|
<lineBasicMaterial color={borderColor} linewidth={lineWidth} depthWrite={false} />
|
|
1341
1445
|
</lineSegments>
|
|
1342
1446
|
|
|
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
1447
|
{/* Always show directory name label */}
|
|
1352
1448
|
<Text
|
|
1353
1449
|
position={[0, textY, textZ]}
|
|
@@ -1658,7 +1754,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1658
1754
|
// This will be corrected in Frame 1 if aspect is different
|
|
1659
1755
|
const initialHeight = calculateFlatCameraHeight(1);
|
|
1660
1756
|
|
|
1661
|
-
console.log('[Spring init] Initializing with 2D position, height:', initialHeight);
|
|
1662
1757
|
return {
|
|
1663
1758
|
camX: 0,
|
|
1664
1759
|
camY: initialHeight,
|
|
@@ -1668,16 +1763,11 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1668
1763
|
lookZ: 0,
|
|
1669
1764
|
config: { tension: 60, friction: 20 },
|
|
1670
1765
|
onStart: () => {
|
|
1671
|
-
// Only allow animations after initial setup is complete
|
|
1672
1766
|
if (hasAppliedInitial.current) {
|
|
1673
|
-
console.log('[Spring onStart] Animation starting - camY:', camY.get());
|
|
1674
1767
|
isAnimatingRef.current = true;
|
|
1675
|
-
} else {
|
|
1676
|
-
console.log('[Spring onStart] Blocked - initialization not complete');
|
|
1677
1768
|
}
|
|
1678
1769
|
},
|
|
1679
1770
|
onRest: () => {
|
|
1680
|
-
console.log('[Spring onRest] Animation finished');
|
|
1681
1771
|
isAnimatingRef.current = false;
|
|
1682
1772
|
},
|
|
1683
1773
|
};
|
|
@@ -1728,23 +1818,11 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1728
1818
|
// When isFlat changes from true to false, animate to 3D view
|
|
1729
1819
|
// Component always starts in 2D, so we only animate the 2D→3D transition
|
|
1730
1820
|
useEffect(() => {
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
// Skip until camera is initialized
|
|
1734
|
-
if (!hasAppliedInitial.current) {
|
|
1735
|
-
console.log('[useEffect] Skipping - not initialized yet');
|
|
1736
|
-
return;
|
|
1737
|
-
}
|
|
1821
|
+
if (!hasAppliedInitial.current) return;
|
|
1738
1822
|
|
|
1739
|
-
// Only animate if isFlat changed from true to false (2D → 3D transition)
|
|
1740
1823
|
const isFlatChanged = prevIsFlatRef.current !== isFlat;
|
|
1824
|
+
if (!isFlatChanged) return;
|
|
1741
1825
|
|
|
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
1826
|
prevIsFlatRef.current = isFlat;
|
|
1749
1827
|
|
|
1750
1828
|
// Calculate target position for 3D view
|
|
@@ -1768,7 +1846,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1768
1846
|
targetZ: 0,
|
|
1769
1847
|
};
|
|
1770
1848
|
|
|
1771
|
-
console.log('[api.start#isFlat-toggle]', newPos);
|
|
1772
1849
|
api.start({
|
|
1773
1850
|
camX: newPos.x,
|
|
1774
1851
|
camY: newPos.y,
|
|
@@ -1838,7 +1915,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1838
1915
|
};
|
|
1839
1916
|
}
|
|
1840
1917
|
|
|
1841
|
-
console.log('[api.start#focus-target]', { focusTarget, isFlat, newPos });
|
|
1842
1918
|
api.start({
|
|
1843
1919
|
camX: newPos.x,
|
|
1844
1920
|
camY: newPos.y,
|
|
@@ -1860,14 +1936,12 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1860
1936
|
// Ensure camera FOV is correct (defaults to 75 before prop applies)
|
|
1861
1937
|
const perspCam = camera as THREE.PerspectiveCamera;
|
|
1862
1938
|
if (perspCam.fov !== 50) {
|
|
1863
|
-
console.log('[Frame 1] Correcting FOV from', perspCam.fov, 'to 50');
|
|
1864
1939
|
perspCam.fov = 50;
|
|
1865
1940
|
perspCam.updateProjectionMatrix();
|
|
1866
1941
|
}
|
|
1867
1942
|
|
|
1868
1943
|
// Calculate initial 2D position with correct aspect ratio
|
|
1869
1944
|
const initialPos = getInitial2DPosition();
|
|
1870
|
-
console.log('[Frame 1] Setting camera to initial 2D position:', initialPos);
|
|
1871
1945
|
|
|
1872
1946
|
camera.position.set(initialPos.x, initialPos.y, initialPos.z);
|
|
1873
1947
|
|
|
@@ -1877,7 +1951,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1877
1951
|
controlsRef.current.update();
|
|
1878
1952
|
|
|
1879
1953
|
// Sync spring to match camera position (use immediate to avoid animation)
|
|
1880
|
-
console.log('[api.start#frame1-immediate]', initialPos);
|
|
1881
1954
|
api.start({
|
|
1882
1955
|
camX: initialPos.x,
|
|
1883
1956
|
camY: initialPos.y,
|
|
@@ -1902,40 +1975,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
1902
1975
|
// Wait for controls and initialization to complete
|
|
1903
1976
|
if (!controlsRef.current || !hasAppliedInitial.current) return;
|
|
1904
1977
|
|
|
1905
|
-
// [debug] log every frame: which branch is active + spring vs camera
|
|
1906
|
-
// position. Throttled to every 250ms. Catches both spring-driven motion
|
|
1907
|
-
// AND external mutation (when no branch is active but position changes).
|
|
1908
|
-
{
|
|
1909
|
-
const w = (
|
|
1910
|
-
globalThis as unknown as {
|
|
1911
|
-
__fileCityFrameLog?: { last: number; lastY: number };
|
|
1912
|
-
}
|
|
1913
|
-
);
|
|
1914
|
-
w.__fileCityFrameLog ??= { last: 0, lastY: NaN };
|
|
1915
|
-
const log = w.__fileCityFrameLog;
|
|
1916
|
-
const now = performance.now();
|
|
1917
|
-
const yChanged =
|
|
1918
|
-
Math.abs(camera.position.y - log.lastY) > 0.5 ||
|
|
1919
|
-
Number.isNaN(log.lastY);
|
|
1920
|
-
if (now - log.last > 250 && yChanged) {
|
|
1921
|
-
log.last = now;
|
|
1922
|
-
log.lastY = camera.position.y;
|
|
1923
|
-
const branch = isOrbitingRef.current
|
|
1924
|
-
? 'orbit'
|
|
1925
|
-
: isTiltingRef.current
|
|
1926
|
-
? 'tilt'
|
|
1927
|
-
: isAnimatingRef.current
|
|
1928
|
-
? 'animating'
|
|
1929
|
-
: 'idle';
|
|
1930
|
-
// eslint-disable-next-line no-console
|
|
1931
|
-
console.log(`[useFrame#${branch}]`, {
|
|
1932
|
-
springY: camY.get(),
|
|
1933
|
-
posY: camera.position.y,
|
|
1934
|
-
springZ: camZ.get(),
|
|
1935
|
-
posZ: camera.position.z,
|
|
1936
|
-
});
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
1978
|
// Handle orbit animation (horizontal rotation along arc)
|
|
1940
1979
|
if (isOrbitingRef.current && orbitParamsRef.current) {
|
|
1941
1980
|
const { centerX, centerZ, distance, height } = orbitParamsRef.current;
|
|
@@ -2002,7 +2041,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
2002
2041
|
const targetHeight = citySize * 1.1;
|
|
2003
2042
|
const targetZ = citySize * 1.3;
|
|
2004
2043
|
|
|
2005
|
-
console.log('[api.start#resetToInitial]', { targetHeight, targetZ });
|
|
2006
2044
|
api.start({
|
|
2007
2045
|
camX: 0,
|
|
2008
2046
|
camY: targetHeight,
|
|
@@ -2018,7 +2056,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
2018
2056
|
const distance = Math.max(effectiveSize * 2, 50);
|
|
2019
2057
|
const height = Math.max(effectiveSize * 1.5, 40);
|
|
2020
2058
|
|
|
2021
|
-
console.log('[api.start#moveTo]', { x, z, height, distance });
|
|
2022
2059
|
api.start({
|
|
2023
2060
|
camX: x,
|
|
2024
2061
|
camY: height,
|
|
@@ -2050,7 +2087,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
2050
2087
|
});
|
|
2051
2088
|
}
|
|
2052
2089
|
const config = options?.duration ? { duration: options.duration } : undefined;
|
|
2053
|
-
console.log('[api.start#setFlatView]', { x, z, height, options });
|
|
2054
2090
|
api.start({
|
|
2055
2091
|
camX: x,
|
|
2056
2092
|
camY: height,
|
|
@@ -2085,7 +2121,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
2085
2121
|
? { duration: options.duration, easing: (t: number) => t }
|
|
2086
2122
|
: { tension: 60, friction: 20 };
|
|
2087
2123
|
|
|
2088
|
-
console.log('[api.start#setTarget]', { x, y, z, newCamX, newCamY, newCamZ });
|
|
2089
2124
|
api.start({
|
|
2090
2125
|
camX: newCamX,
|
|
2091
2126
|
camY: newCamY,
|
|
@@ -2826,6 +2861,8 @@ interface CitySceneProps {
|
|
|
2826
2861
|
growProgress: number;
|
|
2827
2862
|
animationConfig: AnimationConfig;
|
|
2828
2863
|
highlightLayers: HighlightLayer[];
|
|
2864
|
+
/** User-supplied highlight layers (no file-color layers) for visibility logic. */
|
|
2865
|
+
visibilityLayers: HighlightLayer[];
|
|
2829
2866
|
isolationMode: IsolationMode;
|
|
2830
2867
|
heightScaling: HeightScaling;
|
|
2831
2868
|
linearScale: number;
|
|
@@ -2851,6 +2888,7 @@ function CityScene({
|
|
|
2851
2888
|
growProgress,
|
|
2852
2889
|
animationConfig,
|
|
2853
2890
|
highlightLayers,
|
|
2891
|
+
visibilityLayers,
|
|
2854
2892
|
isolationMode,
|
|
2855
2893
|
heightScaling,
|
|
2856
2894
|
linearScale,
|
|
@@ -2884,7 +2922,7 @@ function CityScene({
|
|
|
2884
2922
|
return Math.max(...cityData.buildings.map(b => b.dimensions[1]), 0);
|
|
2885
2923
|
}, [adaptCameraToBuildings, cityData.buildings]);
|
|
2886
2924
|
|
|
2887
|
-
const activeHighlights = useMemo(() => hasActiveHighlights(
|
|
2925
|
+
const activeHighlights = useMemo(() => hasActiveHighlights(visibilityLayers), [visibilityLayers]);
|
|
2888
2926
|
|
|
2889
2927
|
// Helper to check if a path is inside a directory
|
|
2890
2928
|
const isPathInDirectory = useCallback((path: string, directory: string) => {
|
|
@@ -3168,6 +3206,7 @@ function CityScene({
|
|
|
3168
3206
|
staggerIndices={staggerIndices}
|
|
3169
3207
|
focusDirectory={buildingFocusDirectory}
|
|
3170
3208
|
highlightLayers={highlightLayers}
|
|
3209
|
+
visibilityLayers={visibilityLayers}
|
|
3171
3210
|
isolationMode={isolationMode}
|
|
3172
3211
|
defaultBuildingColor={defaultBuildingColor}
|
|
3173
3212
|
/>
|
|
@@ -3180,6 +3219,7 @@ function CityScene({
|
|
|
3180
3219
|
linearScale={linearScale}
|
|
3181
3220
|
flatPatterns={flatPatterns}
|
|
3182
3221
|
highlightLayers={highlightLayers}
|
|
3222
|
+
visibilityLayers={visibilityLayers}
|
|
3183
3223
|
isolationMode={isolationMode}
|
|
3184
3224
|
hasActiveHighlights={activeHighlights}
|
|
3185
3225
|
/>
|
|
@@ -3428,6 +3468,13 @@ export function FileCity3D({
|
|
|
3428
3468
|
const focusColor = resolved.focusColor;
|
|
3429
3469
|
const isolationMode = resolved.isolationMode as IsolationMode;
|
|
3430
3470
|
|
|
3471
|
+
// User-supplied highlight layers only — used for visibility decisions so
|
|
3472
|
+
// file-color layers don't keep every building visible in 'hide' mode.
|
|
3473
|
+
const visibilityLayers = useMemo(
|
|
3474
|
+
() => (externalHighlightLayers ?? []) as HighlightLayer[],
|
|
3475
|
+
[externalHighlightLayers],
|
|
3476
|
+
);
|
|
3477
|
+
|
|
3431
3478
|
// `selectedPath` wins over the deprecated `selectedBuilding` when both are
|
|
3432
3479
|
// set. A path resolves to either a building (file selection) or a district
|
|
3433
3480
|
// (directory selection) — never both.
|
|
@@ -3553,6 +3600,7 @@ export function FileCity3D({
|
|
|
3553
3600
|
growProgress={growProgress}
|
|
3554
3601
|
animationConfig={animationConfig}
|
|
3555
3602
|
highlightLayers={highlightLayers}
|
|
3603
|
+
visibilityLayers={visibilityLayers}
|
|
3556
3604
|
isolationMode={isolationMode}
|
|
3557
3605
|
heightScaling={heightScaling}
|
|
3558
3606
|
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
|
/>
|