@principal-ai/file-city-react 0.5.22 → 0.5.24
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.
- package/dist/components/ArchitectureMapHighlightLayers.d.ts +6 -2
- package/dist/components/ArchitectureMapHighlightLayers.d.ts.map +1 -1
- package/dist/components/ArchitectureMapHighlightLayers.js +74 -2
- package/dist/components/FileCity3D/FileCity3D.d.ts +18 -1
- package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
- package/dist/components/FileCity3D/FileCity3D.js +91 -48
- package/dist/components/FileCity3D/index.d.ts +2 -2
- package/dist/components/FileCity3D/index.d.ts.map +1 -1
- package/dist/components/FileCity3D/index.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/stories/sample-data.d.ts.map +1 -1
- package/dist/stories/sample-data.js +44 -42
- package/dist/utils/visualizationResolution.d.ts +72 -0
- package/dist/utils/visualizationResolution.d.ts.map +1 -0
- package/dist/utils/visualizationResolution.js +100 -0
- package/package.json +1 -1
- package/src/components/ArchitectureMapHighlightLayers.tsx +101 -2
- package/src/components/FileCity3D/FileCity3D.tsx +125 -54
- package/src/components/FileCity3D/index.ts +2 -0
- package/src/index.ts +10 -1
- package/src/stories/2D3DComparison.stories.tsx +527 -0
- package/src/stories/sample-data.ts +47 -45
- package/src/utils/visualizationResolution.ts +170 -0
|
@@ -1,51 +1,52 @@
|
|
|
1
1
|
import { CodeCityBuilderWithGrid, buildFileSystemTreeFromFileInfoList, } from '@principal-ai/file-city-builder';
|
|
2
2
|
// Sample file structure representing a typical project
|
|
3
|
+
// lineCount is estimated as size / 35 (avg ~35 bytes per line)
|
|
3
4
|
const sampleFileStructure = [
|
|
4
5
|
// Source files
|
|
5
|
-
{ path: 'src/index.ts', size: 1500 },
|
|
6
|
-
{ path: 'src/App.tsx', size: 3200 },
|
|
7
|
-
{ path: 'src/components/Header.tsx', size: 1800 },
|
|
8
|
-
{ path: 'src/components/Footer.tsx', size: 1200 },
|
|
9
|
-
{ path: 'src/components/Sidebar.tsx', size: 2100 },
|
|
10
|
-
{ path: 'src/components/Card.tsx', size: 900 },
|
|
11
|
-
{ path: 'src/components/Button.tsx', size: 600 },
|
|
12
|
-
{ path: 'src/utils/helpers.ts', size: 2500 },
|
|
13
|
-
{ path: 'src/utils/api.ts', size: 3100 },
|
|
14
|
-
{ path: 'src/utils/validators.ts', size: 1400 },
|
|
15
|
-
{ path: 'src/hooks/useAuth.ts', size: 800 },
|
|
16
|
-
{ path: 'src/hooks/useData.ts', size: 1100 },
|
|
17
|
-
{ path: 'src/styles/main.css', size: 4500 },
|
|
18
|
-
{ path: 'src/styles/components.css', size: 2800 },
|
|
6
|
+
{ path: 'src/index.ts', size: 1500, lineCount: 45 },
|
|
7
|
+
{ path: 'src/App.tsx', size: 3200, lineCount: 95 },
|
|
8
|
+
{ path: 'src/components/Header.tsx', size: 1800, lineCount: 55 },
|
|
9
|
+
{ path: 'src/components/Footer.tsx', size: 1200, lineCount: 35 },
|
|
10
|
+
{ path: 'src/components/Sidebar.tsx', size: 2100, lineCount: 65 },
|
|
11
|
+
{ path: 'src/components/Card.tsx', size: 900, lineCount: 28 },
|
|
12
|
+
{ path: 'src/components/Button.tsx', size: 600, lineCount: 20 },
|
|
13
|
+
{ path: 'src/utils/helpers.ts', size: 2500, lineCount: 75 },
|
|
14
|
+
{ path: 'src/utils/api.ts', size: 3100, lineCount: 92 },
|
|
15
|
+
{ path: 'src/utils/validators.ts', size: 1400, lineCount: 42 },
|
|
16
|
+
{ path: 'src/hooks/useAuth.ts', size: 800, lineCount: 25 },
|
|
17
|
+
{ path: 'src/hooks/useData.ts', size: 1100, lineCount: 34 },
|
|
18
|
+
{ path: 'src/styles/main.css', size: 4500, lineCount: 180 },
|
|
19
|
+
{ path: 'src/styles/components.css', size: 2800, lineCount: 112 },
|
|
19
20
|
// Test files
|
|
20
|
-
{ path: 'tests/unit/app.test.ts', size: 2200 },
|
|
21
|
-
{ path: 'tests/unit/header.test.ts', size: 1600 },
|
|
22
|
-
{ path: 'tests/unit/footer.test.tsx', size: 1400 },
|
|
23
|
-
{ path: 'tests/integration/api.test.ts', size: 3400 },
|
|
24
|
-
{ path: '__tests__/components.test.tsx', size: 2900 },
|
|
25
|
-
{ path: '__tests__/utils.test.ts', size: 1900 },
|
|
21
|
+
{ path: 'tests/unit/app.test.ts', size: 2200, lineCount: 68 },
|
|
22
|
+
{ path: 'tests/unit/header.test.ts', size: 1600, lineCount: 50 },
|
|
23
|
+
{ path: 'tests/unit/footer.test.tsx', size: 1400, lineCount: 44 },
|
|
24
|
+
{ path: 'tests/integration/api.test.ts', size: 3400, lineCount: 105 },
|
|
25
|
+
{ path: '__tests__/components.test.tsx', size: 2900, lineCount: 90 },
|
|
26
|
+
{ path: '__tests__/utils.test.ts', size: 1900, lineCount: 58 },
|
|
26
27
|
// Config files
|
|
27
|
-
{ path: 'package.json', size: 1200 },
|
|
28
|
-
{ path: 'tsconfig.json', size: 800 },
|
|
29
|
-
{ path: 'webpack.config.js', size: 2100 },
|
|
30
|
-
{ path: '.eslintrc.js', size: 600 },
|
|
31
|
-
{ path: '.prettierrc', size: 200 },
|
|
32
|
-
{ path: 'README.md', size: 3500 },
|
|
28
|
+
{ path: 'package.json', size: 1200, lineCount: 45 },
|
|
29
|
+
{ path: 'tsconfig.json', size: 800, lineCount: 30 },
|
|
30
|
+
{ path: 'webpack.config.js', size: 2100, lineCount: 65 },
|
|
31
|
+
{ path: '.eslintrc.js', size: 600, lineCount: 22 },
|
|
32
|
+
{ path: '.prettierrc', size: 200, lineCount: 8 },
|
|
33
|
+
{ path: 'README.md', size: 3500, lineCount: 120 },
|
|
33
34
|
// Documentation
|
|
34
|
-
{ path: 'docs/README.md', size: 4200 },
|
|
35
|
-
{ path: 'docs/API.md', size: 5100 },
|
|
36
|
-
{ path: 'docs/CONTRIBUTING.md', size: 2300 },
|
|
35
|
+
{ path: 'docs/README.md', size: 4200, lineCount: 140 },
|
|
36
|
+
{ path: 'docs/API.md', size: 5100, lineCount: 170 },
|
|
37
|
+
{ path: 'docs/CONTRIBUTING.md', size: 2300, lineCount: 80 },
|
|
37
38
|
// Build files
|
|
38
|
-
{ path: 'dist/bundle.js', size: 45000 },
|
|
39
|
-
{ path: 'dist/index.html', size: 800 },
|
|
40
|
-
{ path: 'dist/styles.css', size: 12000 },
|
|
39
|
+
{ path: 'dist/bundle.js', size: 45000, lineCount: 1500 },
|
|
40
|
+
{ path: 'dist/index.html', size: 800, lineCount: 30 },
|
|
41
|
+
{ path: 'dist/styles.css', size: 12000, lineCount: 480 },
|
|
41
42
|
// Node modules (sample)
|
|
42
|
-
{ path: 'node_modules/react/index.js', size: 8000 },
|
|
43
|
-
{ path: 'node_modules/react/package.json', size: 1500 },
|
|
44
|
-
{ path: 'node_modules/typescript/lib/typescript.js', size: 65000 },
|
|
45
|
-
{ path: 'node_modules/@types/react/index.d.ts', size: 3200 },
|
|
43
|
+
{ path: 'node_modules/react/index.js', size: 8000, lineCount: 250 },
|
|
44
|
+
{ path: 'node_modules/react/package.json', size: 1500, lineCount: 55 },
|
|
45
|
+
{ path: 'node_modules/typescript/lib/typescript.js', size: 65000, lineCount: 2200 },
|
|
46
|
+
{ path: 'node_modules/@types/react/index.d.ts', size: 3200, lineCount: 100 },
|
|
46
47
|
// Deprecated files
|
|
47
|
-
{ path: 'src/deprecated/OldComponent.tsx', size: 2400 },
|
|
48
|
-
{ path: 'src/deprecated/LegacyAPI.ts', size: 3100 },
|
|
48
|
+
{ path: 'src/deprecated/OldComponent.tsx', size: 2400, lineCount: 72 },
|
|
49
|
+
{ path: 'src/deprecated/LegacyAPI.ts', size: 3100, lineCount: 95 },
|
|
49
50
|
];
|
|
50
51
|
// Convert file structure to FileInfo objects
|
|
51
52
|
function createFileInfoList(files) {
|
|
@@ -54,6 +55,7 @@ function createFileInfoList(files) {
|
|
|
54
55
|
path: file.path,
|
|
55
56
|
relativePath: file.path,
|
|
56
57
|
size: file.size,
|
|
58
|
+
lineCount: file.lineCount,
|
|
57
59
|
extension: file.path.includes('.') ? '.' + (file.path.split('.').pop() || '') : '',
|
|
58
60
|
lastModified: new Date(),
|
|
59
61
|
isDirectory: false,
|
|
@@ -81,10 +83,10 @@ export function createSampleCityData() {
|
|
|
81
83
|
}
|
|
82
84
|
// Smaller sample file structure
|
|
83
85
|
const smallFileStructure = [
|
|
84
|
-
{ path: 'src/index.ts', size: 1500 },
|
|
85
|
-
{ path: 'src/App.tsx', size: 3200 },
|
|
86
|
-
{ path: 'src/utils/helpers.ts', size: 800 },
|
|
87
|
-
{ path: 'package.json', size: 1200 },
|
|
86
|
+
{ path: 'src/index.ts', size: 1500, lineCount: 45 },
|
|
87
|
+
{ path: 'src/App.tsx', size: 3200, lineCount: 95 },
|
|
88
|
+
{ path: 'src/utils/helpers.ts', size: 800, lineCount: 25 },
|
|
89
|
+
{ path: 'package.json', size: 1200, lineCount: 45 },
|
|
88
90
|
];
|
|
89
91
|
let cachedSmallCityData = null;
|
|
90
92
|
// Create a smaller sample for performance testing
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visualization State Resolution
|
|
3
|
+
*
|
|
4
|
+
* This module resolves visualization intent (focus, highlights, file colors)
|
|
5
|
+
* into the primitives needed by the 2D and 3D components.
|
|
6
|
+
*
|
|
7
|
+
* Both ArchitectureMapHighlightLayers (2D) and FileCity3D (3D) use this
|
|
8
|
+
* resolution internally to ensure consistent behavior.
|
|
9
|
+
*
|
|
10
|
+
* See docs/VISUALIZATION_STATE_RESOLUTION.md for detailed documentation.
|
|
11
|
+
*/
|
|
12
|
+
import { HighlightLayer, LayerItem } from '../render/client/drawLayeredBuildings';
|
|
13
|
+
export type { HighlightLayer, LayerItem };
|
|
14
|
+
/**
|
|
15
|
+
* Input highlight layer type - more permissive than the full HighlightLayer
|
|
16
|
+
* to allow both 2D and 3D components to use this resolution.
|
|
17
|
+
* Uses generics to preserve the original layer type through resolution.
|
|
18
|
+
*/
|
|
19
|
+
export interface InputHighlightLayer {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
color: string;
|
|
24
|
+
items: Array<{
|
|
25
|
+
path: string;
|
|
26
|
+
type: 'file' | 'directory';
|
|
27
|
+
}>;
|
|
28
|
+
opacity?: number;
|
|
29
|
+
priority?: number;
|
|
30
|
+
borderWidth?: number;
|
|
31
|
+
dynamic?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Visualization intent - inputs to the resolution
|
|
35
|
+
*/
|
|
36
|
+
export interface VisualizationIntent {
|
|
37
|
+
/** Directory/file to zoom camera to */
|
|
38
|
+
focusPath?: string | null;
|
|
39
|
+
/** Border color for the focused area */
|
|
40
|
+
focusColor?: string | null;
|
|
41
|
+
/** Highlight layers (specific directories/files to highlight) */
|
|
42
|
+
highlightLayers?: InputHighlightLayer[];
|
|
43
|
+
/** Base file type color layers */
|
|
44
|
+
fileColorLayers?: InputHighlightLayer[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolved visualization state - what the components use internally
|
|
48
|
+
*/
|
|
49
|
+
export interface ResolvedVisualizationState {
|
|
50
|
+
/** Combined and filtered highlight layers */
|
|
51
|
+
highlightLayers: InputHighlightLayer[];
|
|
52
|
+
/** Where to point camera (2D: zoomToPath, 3D: focusDirectory) */
|
|
53
|
+
cameraFocusPath: string | null;
|
|
54
|
+
/** Focus area border color */
|
|
55
|
+
focusColor: string | null;
|
|
56
|
+
/** Whether isolation/collapse should be enabled */
|
|
57
|
+
shouldIsolate: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve visualization intent to component primitives.
|
|
61
|
+
*
|
|
62
|
+
* Resolution Rules:
|
|
63
|
+
* 1. When there's a focusPath or highlightLayers, fileColorLayers are filtered
|
|
64
|
+
* to only include files within the visible scope
|
|
65
|
+
* 2. Buildings/files not in any highlight layer automatically collapse/dim
|
|
66
|
+
* 3. Camera focuses on focusPath if provided
|
|
67
|
+
*
|
|
68
|
+
* @param intent - Visualization intent (focus, highlights, file colors)
|
|
69
|
+
* @returns Resolved state with primitives for 2D/3D components
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveVisualizationIntent(intent: VisualizationIntent): ResolvedVisualizationState;
|
|
72
|
+
//# sourceMappingURL=visualizationResolution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visualizationResolution.d.ts","sourceRoot":"","sources":["../../src/utils/visualizationResolution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,uCAAuC,CAAC;AAGlF,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,CAAC;AAE1C;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;KAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iEAAiE;IACjE,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACxC,kCAAkC;IAClC,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACzC;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,6CAA6C;IAC7C,eAAe,EAAE,mBAAmB,EAAE,CAAC;IACvC,iEAAiE;IACjE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,8BAA8B;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,mDAAmD;IACnD,aAAa,EAAE,OAAO,CAAC;CACxB;AAuED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,mBAAmB,GAAG,0BAA0B,CA0BlG"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visualization State Resolution
|
|
3
|
+
*
|
|
4
|
+
* This module resolves visualization intent (focus, highlights, file colors)
|
|
5
|
+
* into the primitives needed by the 2D and 3D components.
|
|
6
|
+
*
|
|
7
|
+
* Both ArchitectureMapHighlightLayers (2D) and FileCity3D (3D) use this
|
|
8
|
+
* resolution internally to ensure consistent behavior.
|
|
9
|
+
*
|
|
10
|
+
* See docs/VISUALIZATION_STATE_RESOLUTION.md for detailed documentation.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Check if a path is within a given scope (directory or file)
|
|
14
|
+
*/
|
|
15
|
+
function isPathInScope(path, scope, scopeType) {
|
|
16
|
+
if (scopeType === 'file') {
|
|
17
|
+
return path === scope;
|
|
18
|
+
}
|
|
19
|
+
// Directory: path is the directory itself or is inside it
|
|
20
|
+
return path === scope || path.startsWith(scope + '/');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get all paths covered by highlight layers
|
|
24
|
+
*/
|
|
25
|
+
function getHighlightedPaths(highlightLayers) {
|
|
26
|
+
const paths = [];
|
|
27
|
+
for (const layer of highlightLayers) {
|
|
28
|
+
if (!layer.enabled)
|
|
29
|
+
continue;
|
|
30
|
+
for (const item of layer.items) {
|
|
31
|
+
paths.push({ path: item.path, type: item.type });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return paths;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Filter file color layers to only include items within the visible scope.
|
|
38
|
+
*
|
|
39
|
+
* Rules:
|
|
40
|
+
* - If there are highlight layers, only show file colors for files within highlighted areas
|
|
41
|
+
* - If there's only a focus path, show file colors for all files within the focus
|
|
42
|
+
* - If neither, show all file colors
|
|
43
|
+
*/
|
|
44
|
+
function filterFileColorLayers(fileColorLayers, focusPath, highlightLayers) {
|
|
45
|
+
const highlightedPaths = getHighlightedPaths(highlightLayers);
|
|
46
|
+
const hasHighlights = highlightedPaths.length > 0;
|
|
47
|
+
// If no focus and no highlights, return all layers unfiltered
|
|
48
|
+
if (!focusPath && !hasHighlights) {
|
|
49
|
+
return fileColorLayers;
|
|
50
|
+
}
|
|
51
|
+
return fileColorLayers
|
|
52
|
+
.map(layer => {
|
|
53
|
+
const filteredItems = layer.items.filter(item => {
|
|
54
|
+
if (hasHighlights) {
|
|
55
|
+
// With highlights: only include items that are within a highlighted path
|
|
56
|
+
return highlightedPaths.some(highlight => isPathInScope(item.path, highlight.path, highlight.type));
|
|
57
|
+
}
|
|
58
|
+
else if (focusPath) {
|
|
59
|
+
// Focus only: include all items within the focus path
|
|
60
|
+
return isPathInScope(item.path, focusPath, 'directory');
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
...layer,
|
|
66
|
+
items: filteredItems,
|
|
67
|
+
};
|
|
68
|
+
})
|
|
69
|
+
.filter(layer => layer.items.length > 0);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolve visualization intent to component primitives.
|
|
73
|
+
*
|
|
74
|
+
* Resolution Rules:
|
|
75
|
+
* 1. When there's a focusPath or highlightLayers, fileColorLayers are filtered
|
|
76
|
+
* to only include files within the visible scope
|
|
77
|
+
* 2. Buildings/files not in any highlight layer automatically collapse/dim
|
|
78
|
+
* 3. Camera focuses on focusPath if provided
|
|
79
|
+
*
|
|
80
|
+
* @param intent - Visualization intent (focus, highlights, file colors)
|
|
81
|
+
* @returns Resolved state with primitives for 2D/3D components
|
|
82
|
+
*/
|
|
83
|
+
export function resolveVisualizationIntent(intent) {
|
|
84
|
+
const { focusPath = null, focusColor = null, highlightLayers = [], fileColorLayers = [], } = intent;
|
|
85
|
+
// Filter file color layers based on focus/highlight scope
|
|
86
|
+
const filteredFileColorLayers = filterFileColorLayers(fileColorLayers, focusPath, highlightLayers);
|
|
87
|
+
// Combine filtered file colors with highlight layers
|
|
88
|
+
// File colors go first (lower priority), highlight layers on top
|
|
89
|
+
const combinedLayers = [...filteredFileColorLayers, ...highlightLayers];
|
|
90
|
+
// Determine if we should isolate (collapse non-highlighted items)
|
|
91
|
+
// Isolate when there's a focus path OR active highlight layers
|
|
92
|
+
const hasActiveHighlights = getHighlightedPaths(highlightLayers).length > 0;
|
|
93
|
+
const shouldIsolate = Boolean(focusPath) || hasActiveHighlights;
|
|
94
|
+
return {
|
|
95
|
+
highlightLayers: combinedLayers,
|
|
96
|
+
cameraFocusPath: focusPath,
|
|
97
|
+
focusColor,
|
|
98
|
+
shouldIsolate,
|
|
99
|
+
};
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { MapInteractionState, MapDisplayOptions } from '../types/react-types';
|
|
24
24
|
import { getDefaultFileColorConfig } from '../utils/fileColorHighlightLayers';
|
|
25
25
|
import { extractIconConfig } from '../utils/fileTypeIcons';
|
|
26
|
+
import { resolveVisualizationIntent } from '../utils/visualizationResolution';
|
|
26
27
|
|
|
27
28
|
const DEFAULT_DISPLAY_OPTIONS: MapDisplayOptions = {
|
|
28
29
|
showGrid: false,
|
|
@@ -105,6 +106,12 @@ export interface ArchitectureMapHighlightLayersProps {
|
|
|
105
106
|
// Border radius configuration
|
|
106
107
|
buildingBorderRadius?: number; // Border radius for buildings (files), default: 0 (sharp corners)
|
|
107
108
|
districtBorderRadius?: number; // Border radius for districts (directories), default: 0 (sharp corners)
|
|
109
|
+
|
|
110
|
+
/** Color to highlight the focused directory border (hex color, e.g. "#3b82f6") */
|
|
111
|
+
focusColor?: string | null;
|
|
112
|
+
|
|
113
|
+
/** Base file type color layers (resolved with highlightLayers) */
|
|
114
|
+
fileColorLayers?: HighlightLayer[];
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
// Spatial Grid for fast hit testing (copied from original for now)
|
|
@@ -289,14 +296,14 @@ interface HitTestCache {
|
|
|
289
296
|
|
|
290
297
|
function ArchitectureMapHighlightLayersInner({
|
|
291
298
|
cityData,
|
|
292
|
-
highlightLayers
|
|
299
|
+
highlightLayers: externalHighlightLayers,
|
|
293
300
|
onLayerToggle,
|
|
294
301
|
focusDirectory = null,
|
|
295
302
|
rootDirectoryName,
|
|
296
303
|
onDirectorySelect,
|
|
297
304
|
onFileClick,
|
|
298
305
|
enableZoom = false,
|
|
299
|
-
zoomToPath
|
|
306
|
+
zoomToPath: externalZoomToPath,
|
|
300
307
|
onZoomComplete,
|
|
301
308
|
zoomAnimationSpeed = 0.12,
|
|
302
309
|
allowZoomToPath = true,
|
|
@@ -319,9 +326,49 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
319
326
|
onHover,
|
|
320
327
|
buildingBorderRadius = 0,
|
|
321
328
|
districtBorderRadius = 0,
|
|
329
|
+
focusColor: externalFocusColor,
|
|
330
|
+
fileColorLayers,
|
|
322
331
|
}: ArchitectureMapHighlightLayersProps) {
|
|
323
332
|
const { theme } = useTheme();
|
|
324
333
|
|
|
334
|
+
// ============================================================================
|
|
335
|
+
// Visualization Resolution
|
|
336
|
+
// Always resolve: combines highlightLayers with fileColorLayers,
|
|
337
|
+
// filtering fileColorLayers based on focus/highlight scope.
|
|
338
|
+
// ============================================================================
|
|
339
|
+
const resolved = useMemo(() => {
|
|
340
|
+
// Cast to InputHighlightLayer[] for resolution - types are compatible at runtime
|
|
341
|
+
const resolution = resolveVisualizationIntent({
|
|
342
|
+
focusPath: externalZoomToPath,
|
|
343
|
+
focusColor: externalFocusColor,
|
|
344
|
+
highlightLayers: (externalHighlightLayers ?? []) as Parameters<typeof resolveVisualizationIntent>[0]['highlightLayers'],
|
|
345
|
+
fileColorLayers: (fileColorLayers ?? []) as Parameters<typeof resolveVisualizationIntent>[0]['fileColorLayers'],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
highlightLayers: resolution.highlightLayers,
|
|
350
|
+
zoomToPath: resolution.cameraFocusPath,
|
|
351
|
+
focusColor: resolution.focusColor,
|
|
352
|
+
shouldDim: resolution.shouldIsolate,
|
|
353
|
+
};
|
|
354
|
+
}, [
|
|
355
|
+
fileColorLayers,
|
|
356
|
+
externalHighlightLayers,
|
|
357
|
+
externalZoomToPath,
|
|
358
|
+
externalFocusColor,
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
// Use resolved values, ensuring layers have required priority for drawing functions
|
|
362
|
+
const highlightLayers: HighlightLayer[] = useMemo(() =>
|
|
363
|
+
resolved.highlightLayers.map((layer, index) => ({
|
|
364
|
+
...layer,
|
|
365
|
+
priority: layer.priority ?? index,
|
|
366
|
+
})) as HighlightLayer[],
|
|
367
|
+
[resolved.highlightLayers]
|
|
368
|
+
);
|
|
369
|
+
const zoomToPath = resolved.zoomToPath;
|
|
370
|
+
const focusColor = resolved.focusColor;
|
|
371
|
+
|
|
325
372
|
// Use theme colors as defaults, with prop overrides
|
|
326
373
|
const resolvedCanvasBackgroundColor = canvasBackgroundColor ?? theme.colors.background;
|
|
327
374
|
const resolvedHoverBorderColor = hoverBorderColor ?? theme.colors.text;
|
|
@@ -1234,6 +1281,55 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
1234
1281
|
iconMap, // Icon configuration map for file type icons
|
|
1235
1282
|
);
|
|
1236
1283
|
|
|
1284
|
+
// Draw focus color border around the zoomToPath target
|
|
1285
|
+
if (focusColor && zoomToPath) {
|
|
1286
|
+
const normalizedPath = zoomToPath.replace(/^\/+|\/+$/g, '');
|
|
1287
|
+
// Find the district or building that matches the path
|
|
1288
|
+
const targetDistrict = filteredCityData.districts?.find(
|
|
1289
|
+
d => d.path === normalizedPath || d.path === zoomToPath,
|
|
1290
|
+
);
|
|
1291
|
+
const targetBuilding = !targetDistrict
|
|
1292
|
+
? filteredCityData.buildings?.find(
|
|
1293
|
+
b => b.path === normalizedPath || b.path === zoomToPath,
|
|
1294
|
+
)
|
|
1295
|
+
: null;
|
|
1296
|
+
|
|
1297
|
+
if (targetDistrict) {
|
|
1298
|
+
// Draw border around district
|
|
1299
|
+
const { minX, maxX, minZ, maxZ } = targetDistrict.worldBounds;
|
|
1300
|
+
const topLeft = worldToCanvas(minX, minZ);
|
|
1301
|
+
const bottomRight = worldToCanvas(maxX, maxZ);
|
|
1302
|
+
const width = bottomRight.x - topLeft.x;
|
|
1303
|
+
const height = bottomRight.y - topLeft.y;
|
|
1304
|
+
|
|
1305
|
+
ctx.save();
|
|
1306
|
+
ctx.strokeStyle = focusColor;
|
|
1307
|
+
ctx.lineWidth = 3 * zoomState.scale;
|
|
1308
|
+
ctx.strokeRect(topLeft.x, topLeft.y, width, height);
|
|
1309
|
+
ctx.restore();
|
|
1310
|
+
} else if (targetBuilding) {
|
|
1311
|
+
// Draw border around building
|
|
1312
|
+
const size = Math.max(targetBuilding.dimensions[0], targetBuilding.dimensions[2]);
|
|
1313
|
+
const halfSize = size / 2;
|
|
1314
|
+
const topLeft = worldToCanvas(
|
|
1315
|
+
targetBuilding.position.x - halfSize,
|
|
1316
|
+
targetBuilding.position.z - halfSize,
|
|
1317
|
+
);
|
|
1318
|
+
const bottomRight = worldToCanvas(
|
|
1319
|
+
targetBuilding.position.x + halfSize,
|
|
1320
|
+
targetBuilding.position.z + halfSize,
|
|
1321
|
+
);
|
|
1322
|
+
const width = bottomRight.x - topLeft.x;
|
|
1323
|
+
const height = bottomRight.y - topLeft.y;
|
|
1324
|
+
|
|
1325
|
+
ctx.save();
|
|
1326
|
+
ctx.strokeStyle = focusColor;
|
|
1327
|
+
ctx.lineWidth = 3 * zoomState.scale;
|
|
1328
|
+
ctx.strokeRect(topLeft.x, topLeft.y, width, height);
|
|
1329
|
+
ctx.restore();
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1237
1333
|
// Performance monitoring end available for debugging
|
|
1238
1334
|
// Performance stats available but not logged to reduce console noise
|
|
1239
1335
|
// Uncomment for debugging: render time, buildings/districts counts, layer counts
|
|
@@ -1268,6 +1364,9 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
1268
1364
|
abstractedPathsSet,
|
|
1269
1365
|
layerIndex,
|
|
1270
1366
|
iconMap,
|
|
1367
|
+
// Focus color border
|
|
1368
|
+
focusColor,
|
|
1369
|
+
zoomToPath,
|
|
1271
1370
|
]);
|
|
1272
1371
|
|
|
1273
1372
|
// Optimized hit testing
|