@principal-ai/file-city-react 0.5.50 → 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,
|
|
@@ -1129,20 +1206,17 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1129
1206
|
// Ensure camera FOV is correct (defaults to 75 before prop applies)
|
|
1130
1207
|
const perspCam = camera;
|
|
1131
1208
|
if (perspCam.fov !== 50) {
|
|
1132
|
-
console.log('[Frame 1] Correcting FOV from', perspCam.fov, 'to 50');
|
|
1133
1209
|
perspCam.fov = 50;
|
|
1134
1210
|
perspCam.updateProjectionMatrix();
|
|
1135
1211
|
}
|
|
1136
1212
|
// Calculate initial 2D position with correct aspect ratio
|
|
1137
1213
|
const initialPos = getInitial2DPosition();
|
|
1138
|
-
console.log('[Frame 1] Setting camera to initial 2D position:', initialPos);
|
|
1139
1214
|
camera.position.set(initialPos.x, initialPos.y, initialPos.z);
|
|
1140
1215
|
// Wait for controls to be ready, then set target and sync spring
|
|
1141
1216
|
if (controlsRef.current) {
|
|
1142
1217
|
controlsRef.current.target.set(initialPos.targetX, initialPos.targetY, initialPos.targetZ);
|
|
1143
1218
|
controlsRef.current.update();
|
|
1144
1219
|
// Sync spring to match camera position (use immediate to avoid animation)
|
|
1145
|
-
console.log('[api.start#frame1-immediate]', initialPos);
|
|
1146
1220
|
api.start({
|
|
1147
1221
|
camX: initialPos.x,
|
|
1148
1222
|
camY: initialPos.y,
|
|
@@ -1164,35 +1238,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1164
1238
|
// Wait for controls and initialization to complete
|
|
1165
1239
|
if (!controlsRef.current || !hasAppliedInitial.current)
|
|
1166
1240
|
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
1241
|
// Handle orbit animation (horizontal rotation along arc)
|
|
1197
1242
|
if (isOrbitingRef.current && orbitParamsRef.current) {
|
|
1198
1243
|
const { centerX, centerZ, distance, height } = orbitParamsRef.current;
|
|
@@ -1250,7 +1295,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1250
1295
|
const resetToInitial = useCallback(() => {
|
|
1251
1296
|
const targetHeight = citySize * 1.1;
|
|
1252
1297
|
const targetZ = citySize * 1.3;
|
|
1253
|
-
console.log('[api.start#resetToInitial]', { targetHeight, targetZ });
|
|
1254
1298
|
api.start({
|
|
1255
1299
|
camX: 0,
|
|
1256
1300
|
camY: targetHeight,
|
|
@@ -1264,7 +1308,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1264
1308
|
const effectiveSize = size ?? citySize * 0.3;
|
|
1265
1309
|
const distance = Math.max(effectiveSize * 2, 50);
|
|
1266
1310
|
const height = Math.max(effectiveSize * 1.5, 40);
|
|
1267
|
-
console.log('[api.start#moveTo]', { x, z, height, distance });
|
|
1268
1311
|
api.start({
|
|
1269
1312
|
camX: x,
|
|
1270
1313
|
camY: height,
|
|
@@ -1294,7 +1337,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1294
1337
|
});
|
|
1295
1338
|
}
|
|
1296
1339
|
const config = options?.duration ? { duration: options.duration } : undefined;
|
|
1297
|
-
console.log('[api.start#setFlatView]', { x, z, height, options });
|
|
1298
1340
|
api.start({
|
|
1299
1341
|
camX: x,
|
|
1300
1342
|
camY: height,
|
|
@@ -1322,7 +1364,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({ citySize, isFlat, fo
|
|
|
1322
1364
|
const animConfig = options?.duration
|
|
1323
1365
|
? { duration: options.duration, easing: (t) => t }
|
|
1324
1366
|
: { tension: 60, friction: 20 };
|
|
1325
|
-
console.log('[api.start#setTarget]', { x, y, z, newCamX, newCamY, newCamZ });
|
|
1326
1367
|
api.start({
|
|
1327
1368
|
camX: newCamX,
|
|
1328
1369
|
camY: newCamY,
|
|
@@ -1770,7 +1811,7 @@ function SelectionRing({ district, centerOffset, color, borderWidth, growProgres
|
|
|
1770
1811
|
const barH = 0.5;
|
|
1771
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 })] })] }));
|
|
1772
1813
|
}
|
|
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, }) {
|
|
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, }) {
|
|
1774
1815
|
const centerOffset = useMemo(() => ({
|
|
1775
1816
|
x: (cityData.bounds.minX + cityData.bounds.maxX) / 2,
|
|
1776
1817
|
z: (cityData.bounds.minZ + cityData.bounds.maxZ) / 2,
|
|
@@ -1782,7 +1823,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
1782
1823
|
return 0;
|
|
1783
1824
|
return Math.max(...cityData.buildings.map(b => b.dimensions[1]), 0);
|
|
1784
1825
|
}, [adaptCameraToBuildings, cityData.buildings]);
|
|
1785
|
-
const activeHighlights = useMemo(() => hasActiveHighlights(
|
|
1826
|
+
const activeHighlights = useMemo(() => hasActiveHighlights(visibilityLayers), [visibilityLayers]);
|
|
1786
1827
|
// Helper to check if a path is inside a directory
|
|
1787
1828
|
const isPathInDirectory = useCallback((path, directory) => {
|
|
1788
1829
|
if (!directory)
|
|
@@ -1974,7 +2015,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
1974
2015
|
// Focus color takes priority, then highlight layer color
|
|
1975
2016
|
const districtColor = (isFocused && buildingFocusColor) ? buildingFocusColor : highlightLayerColor;
|
|
1976
2017
|
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 &&
|
|
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 &&
|
|
1978
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 }))] }));
|
|
1979
2020
|
}
|
|
1980
2021
|
/**
|
|
@@ -2024,6 +2065,9 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
2024
2065
|
const focusDirectory = resolved.focusDirectory;
|
|
2025
2066
|
const focusColor = resolved.focusColor;
|
|
2026
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]);
|
|
2027
2071
|
// `selectedPath` wins over the deprecated `selectedBuilding` when both are
|
|
2028
2072
|
// set. A path resolves to either a building (file selection) or a district
|
|
2029
2073
|
// (directory selection) — never both.
|
|
@@ -2107,6 +2151,6 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
|
|
|
2107
2151
|
height: '100%',
|
|
2108
2152
|
opacity: cameraReady ? 1 : 0,
|
|
2109
2153
|
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) }))] }));
|
|
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) }))] }));
|
|
2111
2155
|
}
|
|
2112
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,40 +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 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
1982
|
// Handle orbit animation (horizontal rotation along arc)
|
|
1940
1983
|
if (isOrbitingRef.current && orbitParamsRef.current) {
|
|
1941
1984
|
const { centerX, centerZ, distance, height } = orbitParamsRef.current;
|
|
@@ -2002,7 +2045,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
2002
2045
|
const targetHeight = citySize * 1.1;
|
|
2003
2046
|
const targetZ = citySize * 1.3;
|
|
2004
2047
|
|
|
2005
|
-
console.log('[api.start#resetToInitial]', { targetHeight, targetZ });
|
|
2006
2048
|
api.start({
|
|
2007
2049
|
camX: 0,
|
|
2008
2050
|
camY: targetHeight,
|
|
@@ -2018,7 +2060,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
2018
2060
|
const distance = Math.max(effectiveSize * 2, 50);
|
|
2019
2061
|
const height = Math.max(effectiveSize * 1.5, 40);
|
|
2020
2062
|
|
|
2021
|
-
console.log('[api.start#moveTo]', { x, z, height, distance });
|
|
2022
2063
|
api.start({
|
|
2023
2064
|
camX: x,
|
|
2024
2065
|
camY: height,
|
|
@@ -2050,7 +2091,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
2050
2091
|
});
|
|
2051
2092
|
}
|
|
2052
2093
|
const config = options?.duration ? { duration: options.duration } : undefined;
|
|
2053
|
-
console.log('[api.start#setFlatView]', { x, z, height, options });
|
|
2054
2094
|
api.start({
|
|
2055
2095
|
camX: x,
|
|
2056
2096
|
camY: height,
|
|
@@ -2085,7 +2125,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
|
|
|
2085
2125
|
? { duration: options.duration, easing: (t: number) => t }
|
|
2086
2126
|
: { tension: 60, friction: 20 };
|
|
2087
2127
|
|
|
2088
|
-
console.log('[api.start#setTarget]', { x, y, z, newCamX, newCamY, newCamZ });
|
|
2089
2128
|
api.start({
|
|
2090
2129
|
camX: newCamX,
|
|
2091
2130
|
camY: newCamY,
|
|
@@ -2826,6 +2865,8 @@ interface CitySceneProps {
|
|
|
2826
2865
|
growProgress: number;
|
|
2827
2866
|
animationConfig: AnimationConfig;
|
|
2828
2867
|
highlightLayers: HighlightLayer[];
|
|
2868
|
+
/** User-supplied highlight layers (no file-color layers) for visibility logic. */
|
|
2869
|
+
visibilityLayers: HighlightLayer[];
|
|
2829
2870
|
isolationMode: IsolationMode;
|
|
2830
2871
|
heightScaling: HeightScaling;
|
|
2831
2872
|
linearScale: number;
|
|
@@ -2851,6 +2892,7 @@ function CityScene({
|
|
|
2851
2892
|
growProgress,
|
|
2852
2893
|
animationConfig,
|
|
2853
2894
|
highlightLayers,
|
|
2895
|
+
visibilityLayers,
|
|
2854
2896
|
isolationMode,
|
|
2855
2897
|
heightScaling,
|
|
2856
2898
|
linearScale,
|
|
@@ -2884,7 +2926,7 @@ function CityScene({
|
|
|
2884
2926
|
return Math.max(...cityData.buildings.map(b => b.dimensions[1]), 0);
|
|
2885
2927
|
}, [adaptCameraToBuildings, cityData.buildings]);
|
|
2886
2928
|
|
|
2887
|
-
const activeHighlights = useMemo(() => hasActiveHighlights(
|
|
2929
|
+
const activeHighlights = useMemo(() => hasActiveHighlights(visibilityLayers), [visibilityLayers]);
|
|
2888
2930
|
|
|
2889
2931
|
// Helper to check if a path is inside a directory
|
|
2890
2932
|
const isPathInDirectory = useCallback((path: string, directory: string) => {
|
|
@@ -3168,6 +3210,7 @@ function CityScene({
|
|
|
3168
3210
|
staggerIndices={staggerIndices}
|
|
3169
3211
|
focusDirectory={buildingFocusDirectory}
|
|
3170
3212
|
highlightLayers={highlightLayers}
|
|
3213
|
+
visibilityLayers={visibilityLayers}
|
|
3171
3214
|
isolationMode={isolationMode}
|
|
3172
3215
|
defaultBuildingColor={defaultBuildingColor}
|
|
3173
3216
|
/>
|
|
@@ -3180,6 +3223,7 @@ function CityScene({
|
|
|
3180
3223
|
linearScale={linearScale}
|
|
3181
3224
|
flatPatterns={flatPatterns}
|
|
3182
3225
|
highlightLayers={highlightLayers}
|
|
3226
|
+
visibilityLayers={visibilityLayers}
|
|
3183
3227
|
isolationMode={isolationMode}
|
|
3184
3228
|
hasActiveHighlights={activeHighlights}
|
|
3185
3229
|
/>
|
|
@@ -3428,6 +3472,13 @@ export function FileCity3D({
|
|
|
3428
3472
|
const focusColor = resolved.focusColor;
|
|
3429
3473
|
const isolationMode = resolved.isolationMode as IsolationMode;
|
|
3430
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
|
+
|
|
3431
3482
|
// `selectedPath` wins over the deprecated `selectedBuilding` when both are
|
|
3432
3483
|
// set. A path resolves to either a building (file selection) or a district
|
|
3433
3484
|
// (directory selection) — never both.
|
|
@@ -3553,6 +3604,7 @@ export function FileCity3D({
|
|
|
3553
3604
|
growProgress={growProgress}
|
|
3554
3605
|
animationConfig={animationConfig}
|
|
3555
3606
|
highlightLayers={highlightLayers}
|
|
3607
|
+
visibilityLayers={visibilityLayers}
|
|
3556
3608
|
isolationMode={isolationMode}
|
|
3557
3609
|
heightScaling={heightScaling}
|
|
3558
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
|
/>
|