@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/file-city-react",
3
- "version": "0.5.22",
3
+ "version": "0.5.24",
4
4
  "type": "module",
5
5
  "description": "React components for File City visualization",
6
6
  "main": "dist/index.js",
@@ -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 = null,
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