@principal-ai/file-city-react 0.3.0

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.
Files changed (51) hide show
  1. package/dist/builder/cityDataUtils.d.ts +15 -0
  2. package/dist/builder/cityDataUtils.d.ts.map +1 -0
  3. package/dist/builder/cityDataUtils.js +348 -0
  4. package/dist/components/ArchitectureMapHighlightLayers.d.ts +63 -0
  5. package/dist/components/ArchitectureMapHighlightLayers.d.ts.map +1 -0
  6. package/dist/components/ArchitectureMapHighlightLayers.js +1040 -0
  7. package/dist/components/CityViewWithReactFlow.d.ts +14 -0
  8. package/dist/components/CityViewWithReactFlow.d.ts.map +1 -0
  9. package/dist/components/CityViewWithReactFlow.js +266 -0
  10. package/dist/config/files.json +996 -0
  11. package/dist/hooks/useCodeCityData.d.ts +21 -0
  12. package/dist/hooks/useCodeCityData.d.ts.map +1 -0
  13. package/dist/hooks/useCodeCityData.js +57 -0
  14. package/dist/index.d.ts +14 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +29 -0
  17. package/dist/render/client/drawLayeredBuildings.d.ts +51 -0
  18. package/dist/render/client/drawLayeredBuildings.d.ts.map +1 -0
  19. package/dist/render/client/drawLayeredBuildings.js +650 -0
  20. package/dist/stories/ArchitectureMapGridLayout.stories.d.ts +73 -0
  21. package/dist/stories/ArchitectureMapGridLayout.stories.d.ts.map +1 -0
  22. package/dist/stories/ArchitectureMapGridLayout.stories.js +345 -0
  23. package/dist/stories/ArchitectureMapHighlightLayers.stories.d.ts +78 -0
  24. package/dist/stories/ArchitectureMapHighlightLayers.stories.d.ts.map +1 -0
  25. package/dist/stories/ArchitectureMapHighlightLayers.stories.js +270 -0
  26. package/dist/stories/CityViewWithReactFlow.stories.d.ts +24 -0
  27. package/dist/stories/CityViewWithReactFlow.stories.d.ts.map +1 -0
  28. package/dist/stories/CityViewWithReactFlow.stories.js +778 -0
  29. package/dist/stories/sample-data.d.ts +4 -0
  30. package/dist/stories/sample-data.d.ts.map +1 -0
  31. package/dist/stories/sample-data.js +268 -0
  32. package/dist/types/react-types.d.ts +17 -0
  33. package/dist/types/react-types.d.ts.map +1 -0
  34. package/dist/types/react-types.js +4 -0
  35. package/dist/utils/fileColorHighlightLayers.d.ts +86 -0
  36. package/dist/utils/fileColorHighlightLayers.d.ts.map +1 -0
  37. package/dist/utils/fileColorHighlightLayers.js +283 -0
  38. package/package.json +49 -0
  39. package/src/builder/cityDataUtils.ts +430 -0
  40. package/src/components/ArchitectureMapHighlightLayers.tsx +1518 -0
  41. package/src/components/CityViewWithReactFlow.tsx +365 -0
  42. package/src/config/files.json +996 -0
  43. package/src/hooks/useCodeCityData.ts +82 -0
  44. package/src/index.ts +64 -0
  45. package/src/render/client/drawLayeredBuildings.ts +946 -0
  46. package/src/stories/ArchitectureMapGridLayout.stories.tsx +410 -0
  47. package/src/stories/ArchitectureMapHighlightLayers.stories.tsx +312 -0
  48. package/src/stories/CityViewWithReactFlow.stories.tsx +787 -0
  49. package/src/stories/sample-data.ts +301 -0
  50. package/src/types/react-types.ts +18 -0
  51. package/src/utils/fileColorHighlightLayers.ts +378 -0
@@ -0,0 +1,1040 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ArchitectureMapHighlightLayers = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const industry_theme_1 = require("@principal-ade/industry-theme");
39
+ const cityDataUtils_1 = require("../builder/cityDataUtils");
40
+ const drawLayeredBuildings_1 = require("../render/client/drawLayeredBuildings");
41
+ const DEFAULT_DISPLAY_OPTIONS = {
42
+ showGrid: false,
43
+ showConnections: true,
44
+ maxConnections: 20,
45
+ gridSize: 50,
46
+ padding: 20,
47
+ };
48
+ // Spatial Grid for fast hit testing (copied from original for now)
49
+ class SpatialGrid {
50
+ constructor(bounds, cellSize = 100) {
51
+ this.grid = new Map();
52
+ this.bounds = bounds;
53
+ this.cellSize = cellSize;
54
+ }
55
+ getCellsForBounds(minX, maxX, minZ, maxZ) {
56
+ const cells = [];
57
+ const startCellX = Math.floor((minX - this.bounds.minX) / this.cellSize);
58
+ const endCellX = Math.floor((maxX - this.bounds.minX) / this.cellSize);
59
+ const startCellZ = Math.floor((minZ - this.bounds.minZ) / this.cellSize);
60
+ const endCellZ = Math.floor((maxZ - this.bounds.minZ) / this.cellSize);
61
+ for (let x = startCellX; x <= endCellX; x++) {
62
+ for (let z = startCellZ; z <= endCellZ; z++) {
63
+ cells.push(`${x},${z}`);
64
+ }
65
+ }
66
+ return cells;
67
+ }
68
+ addBuilding(building) {
69
+ const size = Math.max(building.dimensions[0], building.dimensions[2]);
70
+ const halfSize = size / 2;
71
+ const minX = building.position.x - halfSize;
72
+ const maxX = building.position.x + halfSize;
73
+ const minZ = building.position.z - halfSize;
74
+ const maxZ = building.position.z + halfSize;
75
+ const cells = this.getCellsForBounds(minX, maxX, minZ, maxZ);
76
+ cells.forEach(cellKey => {
77
+ if (!this.grid.has(cellKey)) {
78
+ this.grid.set(cellKey, []);
79
+ }
80
+ const cellItems = this.grid.get(cellKey);
81
+ if (cellItems) {
82
+ cellItems.push(building);
83
+ }
84
+ });
85
+ }
86
+ addDistrict(district) {
87
+ const cells = this.getCellsForBounds(district.worldBounds.minX, district.worldBounds.maxX, district.worldBounds.minZ, district.worldBounds.maxZ);
88
+ cells.forEach(cellKey => {
89
+ if (!this.grid.has(cellKey)) {
90
+ this.grid.set(cellKey, []);
91
+ }
92
+ const cellItems = this.grid.get(cellKey);
93
+ if (cellItems) {
94
+ cellItems.push(district);
95
+ }
96
+ });
97
+ }
98
+ query(x, z, radius = 10) {
99
+ const cells = this.getCellsForBounds(x - radius, x + radius, z - radius, z + radius);
100
+ const results = [];
101
+ const seen = new Set();
102
+ cells.forEach(cellKey => {
103
+ const items = this.grid.get(cellKey);
104
+ if (items) {
105
+ items.forEach(item => {
106
+ const key = 'dimensions' in item ? `b_${item.path}` : `d_${item.path}`;
107
+ if (!seen.has(key)) {
108
+ seen.add(key);
109
+ results.push(item);
110
+ }
111
+ });
112
+ }
113
+ });
114
+ return results;
115
+ }
116
+ clear() {
117
+ this.grid.clear();
118
+ }
119
+ }
120
+ function ArchitectureMapHighlightLayersInner({ cityData, highlightLayers = [], onLayerToggle, focusDirectory = null, rootDirectoryName, onDirectorySelect, onFileClick, enableZoom = false, fullSize = false, showGrid = false, showFileNames = false, className = '', selectiveRender, canvasBackgroundColor, hoverBorderColor, disableOpacityDimming = true, defaultDirectoryColor, defaultBuildingColor, subdirectoryMode, showLayerControls = false, showFileTypeIcons = true, showDirectoryLabels = true, transform = { rotation: 0 }, // Default to no rotation
121
+ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
122
+ const { theme } = (0, industry_theme_1.useTheme)();
123
+ // Use theme colors as defaults, with prop overrides
124
+ const resolvedCanvasBackgroundColor = canvasBackgroundColor ?? theme.colors.background;
125
+ const resolvedHoverBorderColor = hoverBorderColor ?? theme.colors.text;
126
+ const resolvedDefaultDirectoryColor = defaultDirectoryColor ?? theme.colors.backgroundSecondary;
127
+ const resolvedDefaultBuildingColor = defaultBuildingColor ?? theme.colors.muted;
128
+ const canvasRef = (0, react_1.useRef)(null);
129
+ const containerRef = (0, react_1.useRef)(null);
130
+ const [interactionState, setInteractionState] = (0, react_1.useState)({
131
+ hoveredDistrict: null,
132
+ hoveredBuilding: null,
133
+ mousePos: { x: 0, y: 0 },
134
+ });
135
+ const [zoomState, setZoomState] = (0, react_1.useState)({
136
+ scale: 1,
137
+ offsetX: 0,
138
+ offsetY: 0,
139
+ isDragging: false,
140
+ lastMousePos: { x: 0, y: 0 },
141
+ hasMouseMoved: false,
142
+ });
143
+ (0, react_1.useEffect)(() => {
144
+ if (!enableZoom) {
145
+ setZoomState(prev => ({
146
+ ...prev,
147
+ scale: 1,
148
+ offsetX: 0,
149
+ offsetY: 0,
150
+ isDragging: false,
151
+ hasMouseMoved: false,
152
+ }));
153
+ }
154
+ }, [enableZoom]);
155
+ const [hitTestCache, setHitTestCache] = (0, react_1.useState)(null);
156
+ const calculateCanvasResolution = (fileCount, _cityBounds) => {
157
+ const minSize = 400;
158
+ const scaleFactor = Math.sqrt(fileCount / 5);
159
+ const resolution = Math.max(minSize, minSize + scaleFactor * 300);
160
+ return { width: resolution, height: resolution };
161
+ };
162
+ const [canvasSize, setCanvasSize] = (0, react_1.useState)(() => calculateCanvasResolution(cityData?.buildings?.length || 10, cityData?.bounds));
163
+ const [displayOptions] = (0, react_1.useState)({
164
+ ...DEFAULT_DISPLAY_OPTIONS,
165
+ showGrid,
166
+ });
167
+ const filteredCityData = (0, react_1.useMemo)(() => {
168
+ if (!cityData) {
169
+ return undefined;
170
+ }
171
+ let processedData = cityData;
172
+ if (subdirectoryMode?.enabled) {
173
+ const autoCenter = subdirectoryMode.autoCenter === true;
174
+ // Use new multi-filter function if filters are provided
175
+ if (subdirectoryMode.filters && subdirectoryMode.filters.length > 0) {
176
+ processedData = (0, cityDataUtils_1.filterCityDataForMultipleDirectories)(cityData, subdirectoryMode.filters, autoCenter, subdirectoryMode.combineMode || 'union');
177
+ }
178
+ else if (subdirectoryMode.rootPath) {
179
+ // Fallback to single path for backward compatibility
180
+ processedData = (0, cityDataUtils_1.filterCityDataForSubdirectory)(cityData, subdirectoryMode.rootPath, autoCenter);
181
+ }
182
+ }
183
+ return (0, cityDataUtils_1.filterCityDataForSelectiveRender)(processedData, selectiveRender);
184
+ }, [cityData, selectiveRender, subdirectoryMode]);
185
+ const canvasSizingData = (0, react_1.useMemo)(() => {
186
+ if (subdirectoryMode?.enabled && subdirectoryMode.autoCenter !== true && cityData) {
187
+ return cityData;
188
+ }
189
+ return filteredCityData;
190
+ }, [subdirectoryMode?.enabled, subdirectoryMode?.autoCenter, cityData, filteredCityData]);
191
+ // Build hit test cache with spatial indexing
192
+ const buildHitTestCache = (0, react_1.useCallback)((cityData, scale, offsetX, offsetZ, zoomState, abstractedPaths) => {
193
+ const spatialGrid = new SpatialGrid(cityData.bounds);
194
+ // Only add visible buildings to spatial grid
195
+ cityData.buildings.forEach(building => {
196
+ // Check if this building is inside any abstracted directory
197
+ let currentPath = building.path;
198
+ let isAbstracted = false;
199
+ while (currentPath.includes('/')) {
200
+ currentPath = currentPath.substring(0, currentPath.lastIndexOf('/'));
201
+ if (abstractedPaths.has(currentPath)) {
202
+ isAbstracted = true;
203
+ break;
204
+ }
205
+ }
206
+ if (!isAbstracted) {
207
+ spatialGrid.addBuilding(building);
208
+ }
209
+ });
210
+ // Add districts to spatial grid
211
+ cityData.districts.forEach(district => {
212
+ if (!district.path) {
213
+ spatialGrid.addDistrict(district); // Keep root
214
+ return;
215
+ }
216
+ // For hit testing, we need to include abstracted districts too
217
+ // because they have visual covers that users can click on.
218
+ // Only skip children of abstracted directories.
219
+ let isChildOfAbstracted = false;
220
+ for (const abstractedPath of abstractedPaths) {
221
+ if (abstractedPath && district.path.startsWith(abstractedPath + '/')) {
222
+ isChildOfAbstracted = true;
223
+ break;
224
+ }
225
+ }
226
+ if (!isChildOfAbstracted) {
227
+ spatialGrid.addDistrict(district);
228
+ }
229
+ });
230
+ return {
231
+ spatialGrid,
232
+ transformParams: {
233
+ scale,
234
+ offsetX,
235
+ offsetZ,
236
+ zoomScale: zoomState.scale,
237
+ zoomOffsetX: zoomState.offsetX,
238
+ zoomOffsetY: zoomState.offsetY,
239
+ },
240
+ abstractedPaths,
241
+ timestamp: Date.now(),
242
+ };
243
+ }, []);
244
+ // Update canvas size when city data changes
245
+ (0, react_1.useEffect)(() => {
246
+ if (canvasSizingData) {
247
+ const newSize = calculateCanvasResolution(canvasSizingData.buildings.length, canvasSizingData.bounds);
248
+ setCanvasSize(newSize);
249
+ }
250
+ }, [canvasSizingData, subdirectoryMode]);
251
+ // Separate stable and dynamic layers for performance optimization
252
+ const stableLayers = (0, react_1.useMemo)(() => highlightLayers.filter(layer => layer.dynamic !== true), [highlightLayers]);
253
+ const dynamicLayers = (0, react_1.useMemo)(() => highlightLayers.filter(layer => layer.dynamic === true), [highlightLayers]);
254
+ // Combine all layers for rendering (moved up so abstractionLayer can use it)
255
+ const allLayersWithoutAbstraction = (0, react_1.useMemo)(() => [...stableLayers, ...dynamicLayers], [stableLayers, dynamicLayers]);
256
+ // Calculate abstracted directories based on current zoom using tree structure
257
+ const abstractionLayer = (0, react_1.useMemo)(() => {
258
+ // Find the abstraction layer in our highlight layers
259
+ const abstractionLayerDef = highlightLayers.find(layer => layer.id === 'directory-abstraction' && layer.abstractionLayer);
260
+ if (!abstractionLayerDef || !abstractionLayerDef.enabled || !filteredCityData) {
261
+ return null;
262
+ }
263
+ const config = abstractionLayerDef.abstractionConfig;
264
+ // Disable abstraction when zoomed in beyond a certain threshold
265
+ const maxZoomForAbstraction = config?.maxZoomLevel ?? 5.0;
266
+ if (zoomState.scale > maxZoomForAbstraction) {
267
+ return null; // No abstractions when zoomed in
268
+ }
269
+ // Calculate which directories are too small at current zoom
270
+ const abstractedItems = [];
271
+ if (canvasRef.current) {
272
+ const displayWidth = canvasRef.current.clientWidth || canvasSize.width;
273
+ const displayHeight = canvasRef.current.clientHeight || canvasSize.height;
274
+ const coordinateSystemData = subdirectoryMode?.enabled && subdirectoryMode.autoCenter !== true && cityData
275
+ ? cityData
276
+ : filteredCityData;
277
+ const { scale } = calculateScaleAndOffset(coordinateSystemData, displayWidth, displayHeight, displayOptions.padding);
278
+ const totalScale = scale * zoomState.scale;
279
+ // Get all highlighted paths from enabled layers
280
+ const highlightedPaths = new Set();
281
+ allLayersWithoutAbstraction.forEach(layer => {
282
+ if (layer.enabled && layer.id !== 'directory-abstraction') {
283
+ layer.items.forEach(item => {
284
+ if (item.type === 'file') {
285
+ highlightedPaths.add(item.path);
286
+ }
287
+ });
288
+ }
289
+ });
290
+ // Build directory tree
291
+ const nodeMap = new Map();
292
+ const rootNode = {
293
+ path: '',
294
+ district: {
295
+ path: '',
296
+ worldBounds: filteredCityData.bounds,
297
+ fileCount: 0,
298
+ type: 'directory',
299
+ },
300
+ screenSize: { width: Infinity, height: Infinity },
301
+ children: [],
302
+ buildings: [],
303
+ containsHighlights: false,
304
+ shouldAbstract: false,
305
+ };
306
+ nodeMap.set('', rootNode);
307
+ // Create nodes for all districts
308
+ filteredCityData.districts.forEach(district => {
309
+ if (!district.path)
310
+ return;
311
+ const screenWidth = (district.worldBounds.maxX - district.worldBounds.minX) * totalScale;
312
+ const screenHeight = (district.worldBounds.maxZ - district.worldBounds.minZ) * totalScale;
313
+ const node = {
314
+ path: district.path,
315
+ district: district,
316
+ screenSize: { width: screenWidth, height: screenHeight },
317
+ children: [],
318
+ buildings: [],
319
+ containsHighlights: false,
320
+ shouldAbstract: false,
321
+ };
322
+ nodeMap.set(district.path, node);
323
+ });
324
+ // Build parent-child relationships
325
+ nodeMap.forEach((node, path) => {
326
+ if (!path)
327
+ return; // Skip root
328
+ // Find parent path
329
+ const lastSlash = path.lastIndexOf('/');
330
+ const parentPath = lastSlash > 0 ? path.substring(0, lastSlash) : '';
331
+ const parent = nodeMap.get(parentPath);
332
+ if (parent) {
333
+ parent.children.push(node);
334
+ }
335
+ else {
336
+ rootNode.children.push(node);
337
+ }
338
+ });
339
+ // Assign buildings to their directories and check for highlights
340
+ filteredCityData.buildings.forEach(building => {
341
+ const dirPath = building.path.substring(0, building.path.lastIndexOf('/')) || '';
342
+ const node = nodeMap.get(dirPath);
343
+ if (node) {
344
+ node.buildings.push(building);
345
+ if (highlightedPaths.has(building.path)) {
346
+ // Mark this node and all parents as containing highlights
347
+ let current = node;
348
+ let depth = 0;
349
+ while (current && depth < 100) {
350
+ // Add safety limit
351
+ depth++;
352
+ current.containsHighlights = true;
353
+ const parentPath = current.path.lastIndexOf('/') > 0
354
+ ? current.path.substring(0, current.path.lastIndexOf('/'))
355
+ : '';
356
+ if (parentPath === current.path)
357
+ break; // Prevent infinite loop
358
+ current = nodeMap.get(parentPath);
359
+ }
360
+ if (depth >= 100) {
361
+ console.error('[Abstraction] Highlight propagation exceeded depth limit');
362
+ }
363
+ }
364
+ }
365
+ });
366
+ if (config?.allowRootAbstraction) {
367
+ const cityWidth = (filteredCityData.bounds.maxX - filteredCityData.bounds.minX) * totalScale;
368
+ const cityHeight = (filteredCityData.bounds.maxZ - filteredCityData.bounds.minZ) * totalScale;
369
+ const cityArea = cityWidth * cityHeight;
370
+ const canvasArea = displayWidth * displayHeight;
371
+ const minPercentage = config?.minPercentage || 0.01;
372
+ const cityMeetsThreshold = cityArea >= canvasArea * minPercentage;
373
+ if (!cityMeetsThreshold && !highlightedPaths.size) {
374
+ // Abstract the entire city
375
+ // Build meaningful project info text
376
+ const projectInfo = config.projectInfo;
377
+ let displayText = '';
378
+ if (projectInfo?.repoName || projectInfo?.rootDirectoryName) {
379
+ displayText = projectInfo.repoName || projectInfo.rootDirectoryName || 'Project';
380
+ if (projectInfo.currentBranch) {
381
+ displayText += `\n${projectInfo.currentBranch}`;
382
+ }
383
+ }
384
+ else {
385
+ displayText = 'Project Overview';
386
+ }
387
+ abstractedItems.push({
388
+ path: '', // Empty path for root
389
+ type: 'directory',
390
+ renderStrategy: 'cover',
391
+ coverOptions: {
392
+ text: displayText,
393
+ backgroundColor: config?.backgroundColor ?? '#1e40af',
394
+ opacity: 1.0,
395
+ borderRadius: 6,
396
+ textSize: 16,
397
+ },
398
+ });
399
+ // Return early with just the city abstraction
400
+ return {
401
+ ...abstractionLayerDef,
402
+ items: abstractedItems,
403
+ };
404
+ }
405
+ }
406
+ // BFS to determine abstractions for subdirectories
407
+ const queue = [...rootNode.children];
408
+ while (queue.length > 0) {
409
+ const node = queue.shift();
410
+ if (!node)
411
+ continue;
412
+ // Decision logic - using percentage of canvas/viewport area
413
+ // Current screen area of the directory
414
+ const currentArea = node.screenSize.width * node.screenSize.height;
415
+ // Canvas/viewport area
416
+ const canvasArea = displayWidth * displayHeight;
417
+ // Check if current size is at least X% of canvas
418
+ const minPercentage = config?.minPercentage || 0.01; // Default 1% of canvas
419
+ const meetsThreshold = currentArea >= canvasArea * minPercentage;
420
+ if (meetsThreshold) {
421
+ // Large enough - show contents, continue to children
422
+ queue.push(...node.children);
423
+ }
424
+ else if (node.containsHighlights) {
425
+ // Too small but has highlights - show contents, continue to children
426
+ queue.push(...node.children);
427
+ }
428
+ else {
429
+ // Too small and no highlights - abstract this level
430
+ node.shouldAbstract = true;
431
+ const dirName = node.path.split('/').pop() || node.path;
432
+ abstractedItems.push({
433
+ path: node.path,
434
+ type: 'directory',
435
+ renderStrategy: 'cover',
436
+ coverOptions: {
437
+ text: dirName,
438
+ backgroundColor: config?.backgroundColor ?? '#1e40af',
439
+ opacity: 1.0,
440
+ borderRadius: 6,
441
+ textSize: 12,
442
+ },
443
+ });
444
+ // Don't process children - they're covered by this abstraction
445
+ }
446
+ }
447
+ }
448
+ // Return a new layer with calculated items
449
+ return {
450
+ ...abstractionLayerDef,
451
+ items: abstractedItems,
452
+ };
453
+ }, [
454
+ highlightLayers,
455
+ filteredCityData,
456
+ canvasSize,
457
+ displayOptions.padding,
458
+ zoomState.scale,
459
+ subdirectoryMode,
460
+ cityData,
461
+ allLayersWithoutAbstraction,
462
+ ]);
463
+ // Combine all layers for rendering, including calculated abstraction layer
464
+ const allLayers = (0, react_1.useMemo)(() => {
465
+ const layers = [...stableLayers, ...dynamicLayers];
466
+ // Replace abstraction layer with calculated one if it exists
467
+ if (abstractionLayer) {
468
+ const index = layers.findIndex(l => l.id === 'directory-abstraction');
469
+ if (index >= 0) {
470
+ layers[index] = abstractionLayer;
471
+ }
472
+ }
473
+ return layers;
474
+ }, [stableLayers, dynamicLayers, abstractionLayer]);
475
+ // Update hit test cache when geometry or abstraction changes
476
+ (0, react_1.useEffect)(() => {
477
+ if (!filteredCityData)
478
+ return;
479
+ const width = canvasRef.current?.clientWidth || canvasSize.width;
480
+ const height = canvasRef.current?.clientHeight || canvasSize.height;
481
+ const { scale, offsetX, offsetZ } = calculateScaleAndOffset(filteredCityData, width, height, displayOptions.padding);
482
+ // Get abstracted paths from the abstraction layer
483
+ const abstractedPaths = new Set();
484
+ if (abstractionLayer && abstractionLayer.enabled) {
485
+ abstractionLayer.items.forEach(item => {
486
+ if (item.type === 'directory') {
487
+ abstractedPaths.add(item.path);
488
+ }
489
+ });
490
+ }
491
+ const newCache = buildHitTestCache(filteredCityData, scale, offsetX, offsetZ, zoomState, abstractedPaths);
492
+ setHitTestCache(newCache);
493
+ }, [
494
+ filteredCityData,
495
+ canvasSize,
496
+ displayOptions.padding,
497
+ zoomState,
498
+ buildHitTestCache,
499
+ abstractionLayer,
500
+ ]);
501
+ // Main canvas drawing with layer-based highlighting
502
+ (0, react_1.useEffect)(() => {
503
+ if (!canvasRef.current || !filteredCityData) {
504
+ return;
505
+ }
506
+ const canvas = canvasRef.current;
507
+ const ctx = canvas.getContext('2d');
508
+ if (!ctx) {
509
+ return;
510
+ }
511
+ // Performance monitoring start available for debugging
512
+ const displayWidth = canvas.clientWidth || canvasSize.width;
513
+ const displayHeight = canvas.clientHeight || canvasSize.height;
514
+ canvas.width = displayWidth;
515
+ canvas.height = displayHeight;
516
+ ctx.fillStyle = resolvedCanvasBackgroundColor;
517
+ ctx.fillRect(0, 0, displayWidth, displayHeight);
518
+ if (displayOptions.showGrid) {
519
+ (0, drawLayeredBuildings_1.drawGrid)(ctx, displayWidth, displayHeight, displayOptions.gridSize);
520
+ }
521
+ const coordinateSystemData = subdirectoryMode?.enabled && subdirectoryMode.autoCenter !== true && cityData
522
+ ? cityData
523
+ : filteredCityData;
524
+ const { scale, offsetX, offsetZ } = calculateScaleAndOffset(coordinateSystemData, displayWidth, displayHeight, displayOptions.padding);
525
+ const worldToCanvas = (x, z) => ({
526
+ x: ((x - coordinateSystemData.bounds.minX) * scale + offsetX) * zoomState.scale +
527
+ zoomState.offsetX,
528
+ y: ((z - coordinateSystemData.bounds.minZ) * scale + offsetZ) * zoomState.scale +
529
+ zoomState.offsetY,
530
+ });
531
+ // Get abstracted paths for filtering child districts
532
+ const abstractedPathsForDistricts = new Set();
533
+ const abstractionLayerForDistricts = allLayers.find(l => l.id === 'directory-abstraction');
534
+ if (abstractionLayerForDistricts && abstractionLayerForDistricts.enabled) {
535
+ abstractionLayerForDistricts.items.forEach(item => {
536
+ if (item.type === 'directory') {
537
+ abstractedPathsForDistricts.add(item.path);
538
+ }
539
+ });
540
+ }
541
+ // Keep abstracted districts (for covers) but filter out their children
542
+ let visibleDistricts = abstractedPathsForDistricts.size > 0
543
+ ? filteredCityData.districts.filter(district => {
544
+ // Check for root abstraction first
545
+ if (abstractedPathsForDistricts.has('')) {
546
+ // If root is abstracted, only show root district
547
+ return !district.path || district.path === '';
548
+ }
549
+ if (!district.path)
550
+ return true; // Keep root
551
+ // Keep the abstracted district itself (we need it for the cover)
552
+ if (abstractedPathsForDistricts.has(district.path)) {
553
+ return true;
554
+ }
555
+ // Filter out children of abstracted directories
556
+ for (const abstractedPath of abstractedPathsForDistricts) {
557
+ if (district.path.startsWith(abstractedPath + '/')) {
558
+ return false; // Skip child of abstracted directory
559
+ }
560
+ }
561
+ return true;
562
+ })
563
+ : filteredCityData.districts;
564
+ // If root is abstracted and there's no root district, create one for the cover
565
+ if (abstractedPathsForDistricts.has('')) {
566
+ const hasRootDistrict = visibleDistricts.some(d => !d.path || d.path === '');
567
+ if (!hasRootDistrict) {
568
+ visibleDistricts = [
569
+ {
570
+ path: '',
571
+ worldBounds: filteredCityData.bounds,
572
+ fileCount: filteredCityData.buildings.length, // Total file count
573
+ type: 'directory',
574
+ },
575
+ ...visibleDistricts,
576
+ ];
577
+ }
578
+ }
579
+ // Draw districts with layer support
580
+ (0, drawLayeredBuildings_1.drawLayeredDistricts)(ctx, visibleDistricts, worldToCanvas, scale * zoomState.scale, allLayers, interactionState.hoveredDistrict, fullSize, resolvedDefaultDirectoryColor, filteredCityData.metadata.layoutConfig, abstractedPathsForDistricts, // Pass abstracted paths to skip labels
581
+ showDirectoryLabels, districtBorderRadius);
582
+ // Get abstracted directory paths for filtering from the actual layers being rendered
583
+ const abstractedPaths = new Set();
584
+ const activeAbstractionLayer = allLayers.find(l => l.id === 'directory-abstraction');
585
+ if (activeAbstractionLayer && activeAbstractionLayer.enabled) {
586
+ activeAbstractionLayer.items.forEach(item => {
587
+ if (item.type === 'directory') {
588
+ abstractedPaths.add(item.path);
589
+ }
590
+ });
591
+ }
592
+ // Filter out buildings that are in abstracted directories
593
+ const visibleBuildings = abstractedPaths.size > 0
594
+ ? filteredCityData.buildings.filter(building => {
595
+ // Check for root abstraction first
596
+ if (abstractedPaths.has('')) {
597
+ // If root is abstracted, hide all buildings
598
+ return false;
599
+ }
600
+ const buildingDir = building.path.substring(0, building.path.lastIndexOf('/')) || '';
601
+ // Simple direct check first
602
+ for (const abstractedPath of abstractedPaths) {
603
+ if (buildingDir === abstractedPath) {
604
+ return false;
605
+ }
606
+ // Also check if building is in a subdirectory of abstracted path
607
+ if (buildingDir.startsWith(abstractedPath + '/')) {
608
+ return false;
609
+ }
610
+ }
611
+ return true;
612
+ })
613
+ : filteredCityData.buildings;
614
+ // Log only if we see buildings that should have been filtered
615
+ if (abstractedPaths.size > 0) {
616
+ const suspiciousBuildings = visibleBuildings.filter(building => {
617
+ const buildingDir = building.path.substring(0, building.path.lastIndexOf('/')) || '';
618
+ // Check if this building's parent is visually covered
619
+ for (const abstractedPath of abstractedPaths) {
620
+ if (buildingDir === abstractedPath || buildingDir.startsWith(abstractedPath + '/')) {
621
+ return true;
622
+ }
623
+ }
624
+ return false;
625
+ });
626
+ if (suspiciousBuildings.length > 0) {
627
+ console.error(`[Building Filter] WARNING: ${suspiciousBuildings.length} buildings are being rendered in abstracted directories!`);
628
+ suspiciousBuildings.slice(0, 3).forEach(building => {
629
+ console.error(` - ${building.path}`);
630
+ });
631
+ }
632
+ }
633
+ // Draw buildings with layer support
634
+ (0, drawLayeredBuildings_1.drawLayeredBuildings)(ctx, visibleBuildings, worldToCanvas, scale * zoomState.scale, allLayers, interactionState.hoveredBuilding, resolvedDefaultBuildingColor, showFileNames, resolvedHoverBorderColor, disableOpacityDimming, showFileTypeIcons, buildingBorderRadius);
635
+ // Performance monitoring end available for debugging
636
+ // Performance stats available but not logged to reduce console noise
637
+ // Uncomment for debugging: render time, buildings/districts counts, layer counts
638
+ }, [
639
+ filteredCityData,
640
+ canvasSize,
641
+ displayOptions,
642
+ zoomState,
643
+ selectiveRender,
644
+ rootDirectoryName,
645
+ highlightLayers,
646
+ interactionState.hoveredBuilding,
647
+ interactionState.hoveredDistrict,
648
+ resolvedDefaultDirectoryColor,
649
+ showFileNames,
650
+ fullSize,
651
+ resolvedHoverBorderColor,
652
+ disableOpacityDimming,
653
+ focusDirectory,
654
+ subdirectoryMode,
655
+ cityData,
656
+ resolvedCanvasBackgroundColor,
657
+ showDirectoryLabels,
658
+ allLayers,
659
+ buildingBorderRadius,
660
+ resolvedDefaultBuildingColor,
661
+ districtBorderRadius,
662
+ showFileTypeIcons,
663
+ ]);
664
+ // Optimized hit testing
665
+ const performHitTest = (0, react_1.useCallback)((canvasX, canvasY) => {
666
+ if (!hitTestCache || !filteredCityData) {
667
+ return { hoveredBuilding: null, hoveredDistrict: null };
668
+ }
669
+ const width = canvasRef.current?.clientWidth || canvasSize.width;
670
+ const height = canvasRef.current?.clientHeight || canvasSize.height;
671
+ const coordinateSystemData = subdirectoryMode?.enabled && subdirectoryMode.autoCenter !== true && cityData
672
+ ? cityData
673
+ : filteredCityData;
674
+ const { scale, offsetX, offsetZ } = calculateScaleAndOffset(coordinateSystemData, width, height, displayOptions.padding);
675
+ const screenX = (canvasX - zoomState.offsetX) / zoomState.scale;
676
+ const screenY = (canvasY - zoomState.offsetY) / zoomState.scale;
677
+ const worldX = (screenX - offsetX) / scale + coordinateSystemData.bounds.minX;
678
+ const worldZ = (screenY - offsetZ) / scale + coordinateSystemData.bounds.minZ;
679
+ const nearbyItems = hitTestCache.spatialGrid.query(worldX, worldZ, 20);
680
+ let hoveredBuilding = null;
681
+ let hoveredDistrict = null;
682
+ let minBuildingDistance = Infinity;
683
+ let deepestDistrictLevel = -1;
684
+ for (const item of nearbyItems) {
685
+ if ('dimensions' in item) {
686
+ const building = item;
687
+ const size = Math.max(building.dimensions[0], building.dimensions[2]);
688
+ const halfSize = size / 2;
689
+ if (worldX >= building.position.x - halfSize &&
690
+ worldX <= building.position.x + halfSize &&
691
+ worldZ >= building.position.z - halfSize &&
692
+ worldZ <= building.position.z + halfSize) {
693
+ const distance = Math.sqrt(Math.pow(worldX - building.position.x, 2) + Math.pow(worldZ - building.position.z, 2));
694
+ if (distance < minBuildingDistance) {
695
+ minBuildingDistance = distance;
696
+ hoveredBuilding = building;
697
+ }
698
+ }
699
+ }
700
+ }
701
+ for (const item of nearbyItems) {
702
+ if (!('dimensions' in item)) {
703
+ const district = item;
704
+ const districtPath = district.path || '';
705
+ const isRoot = !districtPath || districtPath === '';
706
+ // Skip root districts UNLESS they are abstracted (have covers)
707
+ if (isRoot && !hitTestCache.abstractedPaths.has(''))
708
+ continue;
709
+ if (worldX >= district.worldBounds.minX &&
710
+ worldX <= district.worldBounds.maxX &&
711
+ worldZ >= district.worldBounds.minZ &&
712
+ worldZ <= district.worldBounds.maxZ) {
713
+ const level = districtPath.split('/').filter(Boolean).length;
714
+ if (level > deepestDistrictLevel) {
715
+ deepestDistrictLevel = level;
716
+ hoveredDistrict = district;
717
+ }
718
+ }
719
+ }
720
+ }
721
+ return { hoveredBuilding, hoveredDistrict };
722
+ }, [
723
+ hitTestCache,
724
+ filteredCityData,
725
+ canvasSize,
726
+ displayOptions.padding,
727
+ zoomState,
728
+ subdirectoryMode,
729
+ cityData,
730
+ ]);
731
+ // Mouse event handlers
732
+ // Call onHover callback when interaction state changes
733
+ (0, react_1.useEffect)(() => {
734
+ if (onHover && interactionState) {
735
+ const fileTooltip = interactionState.hoveredBuilding
736
+ ? {
737
+ text: interactionState.hoveredBuilding.path.split('/').pop() ||
738
+ interactionState.hoveredBuilding.path,
739
+ }
740
+ : null;
741
+ const directoryTooltip = interactionState.hoveredDistrict && interactionState.hoveredDistrict.path !== '/'
742
+ ? { text: interactionState.hoveredDistrict.path || '/' }
743
+ : null;
744
+ onHover({
745
+ hoveredDistrict: interactionState.hoveredDistrict,
746
+ hoveredBuilding: interactionState.hoveredBuilding,
747
+ mousePos: interactionState.mousePos,
748
+ fileTooltip,
749
+ directoryTooltip,
750
+ fileCount: interactionState.hoveredDistrict
751
+ ? Math.round(interactionState.hoveredDistrict.fileCount || 0)
752
+ : null,
753
+ });
754
+ }
755
+ }, [interactionState, onHover]);
756
+ // Helper function to transform mouse coordinates based on rotation/flip
757
+ const transformMouseCoordinates = (0, react_1.useCallback)((x, y, canvasWidth, canvasHeight) => {
758
+ const centerX = canvasWidth / 2;
759
+ const centerY = canvasHeight / 2;
760
+ // Translate to center
761
+ let transformedX = x - centerX;
762
+ let transformedY = y - centerY;
763
+ // Apply inverse rotation (negative angle to undo visual rotation)
764
+ const rotationDegrees = transform?.rotation || 0;
765
+ if (rotationDegrees) {
766
+ const angle = -(rotationDegrees * Math.PI) / 180;
767
+ const cos = Math.cos(angle);
768
+ const sin = Math.sin(angle);
769
+ const newX = transformedX * cos - transformedY * sin;
770
+ const newY = transformedX * sin + transformedY * cos;
771
+ transformedX = newX;
772
+ transformedY = newY;
773
+ }
774
+ // Apply inverse flips
775
+ if (transform?.flipHorizontal) {
776
+ transformedX = -transformedX;
777
+ }
778
+ if (transform?.flipVertical) {
779
+ transformedY = -transformedY;
780
+ }
781
+ // Translate back from center
782
+ transformedX += centerX;
783
+ transformedY += centerY;
784
+ return { x: transformedX, y: transformedY };
785
+ }, [transform]);
786
+ const handleMouseMoveInternal = (0, react_1.useCallback)((e) => {
787
+ if (!canvasRef.current || !containerRef.current || !filteredCityData || zoomState.isDragging)
788
+ return;
789
+ // Get the container rect for mouse position
790
+ const containerRect = containerRef.current.getBoundingClientRect();
791
+ // Get mouse position relative to the container
792
+ const rawX = e.clientX - containerRect.left;
793
+ const rawY = e.clientY - containerRect.top;
794
+ // Use canvas dimensions for transformation since that's what the hit testing uses
795
+ const canvasWidth = canvasRef.current.width;
796
+ const canvasHeight = canvasRef.current.height;
797
+ // Transform the mouse coordinates to account for rotation/flips
798
+ const { x, y } = transformMouseCoordinates(rawX, rawY, canvasWidth, canvasHeight);
799
+ const { hoveredBuilding, hoveredDistrict } = performHitTest(x, y);
800
+ setInteractionState(prev => {
801
+ const hoveredItemChanged = prev.hoveredBuilding !== hoveredBuilding || prev.hoveredDistrict !== hoveredDistrict;
802
+ const positionChanged = prev.mousePos.x !== x || prev.mousePos.y !== y;
803
+ if (!hoveredItemChanged && !positionChanged) {
804
+ return prev;
805
+ }
806
+ // Tooltip data will be calculated in onHover callback
807
+ return {
808
+ hoveredBuilding,
809
+ hoveredDistrict,
810
+ mousePos: { x, y },
811
+ };
812
+ });
813
+ }, [filteredCityData, zoomState.isDragging, performHitTest, transformMouseCoordinates]);
814
+ const handleMouseMove = (0, react_1.useCallback)((e) => {
815
+ if (enableZoom && zoomState.isDragging) {
816
+ const rawDeltaX = e.clientX - zoomState.lastMousePos.x;
817
+ const rawDeltaY = e.clientY - zoomState.lastMousePos.y;
818
+ // Transform the drag deltas based on rotation/flips
819
+ let deltaX = rawDeltaX;
820
+ let deltaY = rawDeltaY;
821
+ const rotationDegrees = transform?.rotation || 0;
822
+ if (rotationDegrees) {
823
+ // Apply inverse rotation to the deltas
824
+ const angle = -(rotationDegrees * Math.PI) / 180;
825
+ const cos = Math.cos(angle);
826
+ const sin = Math.sin(angle);
827
+ deltaX = rawDeltaX * cos - rawDeltaY * sin;
828
+ deltaY = rawDeltaX * sin + rawDeltaY * cos;
829
+ }
830
+ // Apply inverse flips
831
+ if (transform?.flipHorizontal) {
832
+ deltaX = -deltaX;
833
+ }
834
+ if (transform?.flipVertical) {
835
+ deltaY = -deltaY;
836
+ }
837
+ setZoomState(prev => ({
838
+ ...prev,
839
+ offsetX: prev.offsetX + deltaX,
840
+ offsetY: prev.offsetY + deltaY,
841
+ lastMousePos: { x: e.clientX, y: e.clientY },
842
+ hasMouseMoved: true,
843
+ }));
844
+ return;
845
+ }
846
+ handleMouseMoveInternal(e);
847
+ }, [enableZoom, zoomState.isDragging, zoomState.lastMousePos, handleMouseMoveInternal, transform]);
848
+ const handleClick = () => {
849
+ if (zoomState.hasMouseMoved) {
850
+ return;
851
+ }
852
+ if (interactionState.hoveredBuilding && onFileClick) {
853
+ /*const fullPath = subdirectoryMode?.enabled && subdirectoryMode.rootPath
854
+ ? `${subdirectoryMode.rootPath}/${interactionState.hoveredBuilding.path}`
855
+ : interactionState.hoveredBuilding.path;*/
856
+ onFileClick(interactionState.hoveredBuilding.path, 'file');
857
+ return;
858
+ }
859
+ if (interactionState.hoveredDistrict) {
860
+ const districtPath = interactionState.hoveredDistrict.path || '';
861
+ const isRoot = !districtPath || districtPath === '';
862
+ const fullPath = subdirectoryMode?.enabled && subdirectoryMode.rootPath && !isRoot
863
+ ? `${subdirectoryMode.rootPath}/${districtPath}`
864
+ : districtPath;
865
+ if (onFileClick && !isRoot) {
866
+ onFileClick(fullPath, 'directory');
867
+ }
868
+ else if (onDirectorySelect) {
869
+ onDirectorySelect(focusDirectory === fullPath ? null : fullPath);
870
+ }
871
+ }
872
+ };
873
+ const handleMouseLeave = (0, react_1.useCallback)(() => {
874
+ setInteractionState(prev => ({
875
+ ...prev,
876
+ hoveredDistrict: null,
877
+ hoveredBuilding: null,
878
+ }));
879
+ }, []);
880
+ const handleWheel = (e) => {
881
+ if (!enableZoom || !canvasRef.current) {
882
+ return;
883
+ }
884
+ const rect = canvasRef.current.getBoundingClientRect();
885
+ const mouseX = e.clientX - rect.left;
886
+ const mouseY = e.clientY - rect.top;
887
+ const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
888
+ const newScale = Math.max(0.1, Math.min(5, zoomState.scale * zoomFactor));
889
+ const scaleChange = newScale / zoomState.scale;
890
+ const newOffsetX = mouseX - (mouseX - zoomState.offsetX) * scaleChange;
891
+ const newOffsetY = mouseY - (mouseY - zoomState.offsetY) * scaleChange;
892
+ setZoomState(prev => ({
893
+ ...prev,
894
+ scale: newScale,
895
+ offsetX: newOffsetX,
896
+ offsetY: newOffsetY,
897
+ }));
898
+ };
899
+ const handleMouseDown = (e) => {
900
+ setZoomState(prev => ({
901
+ ...prev,
902
+ isDragging: enableZoom ? true : false,
903
+ lastMousePos: enableZoom ? { x: e.clientX, y: e.clientY } : prev.lastMousePos,
904
+ hasMouseMoved: false,
905
+ }));
906
+ };
907
+ const handleMouseUp = (0, react_1.useCallback)(() => {
908
+ setZoomState(prev => ({ ...prev, isDragging: false }));
909
+ }, []);
910
+ const interactionCursor = (0, react_1.useMemo)(() => {
911
+ if (enableZoom) {
912
+ if (zoomState.isDragging) {
913
+ return 'grabbing';
914
+ }
915
+ if (interactionState.hoveredBuilding || interactionState.hoveredDistrict) {
916
+ return 'pointer';
917
+ }
918
+ return 'grab';
919
+ }
920
+ if (interactionState.hoveredBuilding || interactionState.hoveredDistrict) {
921
+ return 'pointer';
922
+ }
923
+ return 'default';
924
+ }, [
925
+ enableZoom,
926
+ zoomState.isDragging,
927
+ interactionState.hoveredBuilding,
928
+ interactionState.hoveredDistrict,
929
+ ]);
930
+ if (!filteredCityData) {
931
+ return (react_1.default.createElement("div", { className: className, style: {
932
+ width: '100%',
933
+ height: '100%',
934
+ minHeight: '250px',
935
+ backgroundColor: theme.colors.backgroundSecondary,
936
+ borderRadius: `${theme.radii[2]}px`,
937
+ padding: `${theme.space[4]}px`,
938
+ display: 'flex',
939
+ alignItems: 'center',
940
+ justifyContent: 'center',
941
+ } },
942
+ react_1.default.createElement("div", { style: { color: theme.colors.textMuted, fontFamily: theme.fonts.body } }, "No city data available")));
943
+ }
944
+ return (react_1.default.createElement("div", { ref: containerRef, className: className, style: {
945
+ position: 'relative',
946
+ width: '100%',
947
+ height: '100%',
948
+ overflow: 'hidden',
949
+ backgroundColor: resolvedCanvasBackgroundColor,
950
+ transform: (() => {
951
+ const transforms = [];
952
+ // Apply rotation
953
+ const rotationDegrees = transform?.rotation || 0;
954
+ if (rotationDegrees) {
955
+ transforms.push(`rotate(${rotationDegrees}deg)`);
956
+ }
957
+ // Apply flips
958
+ if (transform?.flipHorizontal) {
959
+ transforms.push('scaleX(-1)');
960
+ }
961
+ if (transform?.flipVertical) {
962
+ transforms.push('scaleY(-1)');
963
+ }
964
+ return transforms.length > 0 ? transforms.join(' ') : undefined;
965
+ })(),
966
+ } },
967
+ showLayerControls && highlightLayers.length > 0 && (react_1.default.createElement("div", { style: {
968
+ position: 'absolute',
969
+ bottom: `${theme.space[4]}px`,
970
+ left: `${theme.space[4]}px`,
971
+ zIndex: 10,
972
+ display: 'flex',
973
+ flexDirection: 'column',
974
+ gap: `${theme.space[2]}px`,
975
+ } }, highlightLayers.map(layer => (react_1.default.createElement("button", { key: layer.id, onClick: () => onLayerToggle?.(layer.id, !layer.enabled), style: {
976
+ padding: `${theme.space[2]}px ${theme.space[3]}px`,
977
+ borderRadius: `${theme.radii[2]}px`,
978
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
979
+ transition: 'all 0.2s ease',
980
+ display: 'flex',
981
+ alignItems: 'center',
982
+ gap: `${theme.space[2]}px`,
983
+ fontSize: `${theme.fontSizes[0]}px`,
984
+ fontWeight: theme.fontWeights.medium,
985
+ fontFamily: theme.fonts.body,
986
+ backgroundColor: layer.enabled
987
+ ? theme.colors.backgroundSecondary
988
+ : theme.colors.background,
989
+ color: layer.enabled ? theme.colors.text : theme.colors.textSecondary,
990
+ border: `2px solid ${layer.enabled ? layer.color : theme.colors.border}`,
991
+ minWidth: '120px',
992
+ cursor: 'pointer',
993
+ }, title: `Toggle ${layer.name}` },
994
+ react_1.default.createElement("div", { style: {
995
+ width: '12px',
996
+ height: '12px',
997
+ borderRadius: '50%',
998
+ backgroundColor: layer.color,
999
+ opacity: layer.enabled ? 1 : 0.4,
1000
+ transition: 'opacity 0.2s ease',
1001
+ } }),
1002
+ react_1.default.createElement("span", { style: { textAlign: 'left', flex: 1 } }, layer.name),
1003
+ layer.items && layer.items.length > 0 && (react_1.default.createElement("span", { style: {
1004
+ fontSize: `${theme.fontSizes[0]}px`,
1005
+ color: layer.enabled ? theme.colors.textSecondary : theme.colors.textMuted,
1006
+ } }, layer.items.length))))))),
1007
+ react_1.default.createElement("canvas", { ref: canvasRef, style: {
1008
+ position: 'absolute',
1009
+ top: 0,
1010
+ left: 0,
1011
+ width: '100%',
1012
+ height: '100%',
1013
+ objectFit: 'contain',
1014
+ zIndex: 1,
1015
+ } }),
1016
+ react_1.default.createElement("div", { style: {
1017
+ position: 'absolute',
1018
+ top: 0,
1019
+ left: 0,
1020
+ width: '100%',
1021
+ height: '100%',
1022
+ zIndex: 2,
1023
+ cursor: interactionCursor,
1024
+ }, onMouseMove: handleMouseMove, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onClick: handleClick, onMouseLeave: handleMouseLeave, onWheel: handleWheel })));
1025
+ }
1026
+ function calculateScaleAndOffset(cityData, width, height, padding) {
1027
+ const cityWidth = cityData.bounds.maxX - cityData.bounds.minX;
1028
+ const cityDepth = cityData.bounds.maxZ - cityData.bounds.minZ;
1029
+ const horizontalPadding = padding;
1030
+ const verticalPadding = padding * 2;
1031
+ const scaleX = (width - horizontalPadding) / cityDepth;
1032
+ const scaleZ = (height - verticalPadding) / cityWidth;
1033
+ const scale = Math.min(scaleX, scaleZ);
1034
+ const scaledCityWidth = cityDepth * scale;
1035
+ const scaledCityHeight = cityWidth * scale;
1036
+ const offsetX = (width - scaledCityWidth) / 2;
1037
+ const offsetZ = (height - scaledCityHeight) / 2;
1038
+ return { scale, offsetX, offsetZ };
1039
+ }
1040
+ exports.ArchitectureMapHighlightLayers = ArchitectureMapHighlightLayersInner;