@principal-ai/file-city-react 0.5.35 → 0.5.37

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.
@@ -7,7 +7,7 @@
7
7
  * Supports animated transition from 2D (flat) to 3D (grown buildings).
8
8
  */
9
9
  import React from 'react';
10
- import type { CityData, CityBuilding, CityDistrict } from '@principal-ai/file-city-builder';
10
+ import type { CityData, CityBuilding, CityDistrict, HighlightLayer as BuilderHighlightLayer, LayerItem, LayerRenderStrategy } from '@principal-ai/file-city-builder';
11
11
  import type { ThreeElements } from '@react-three/fiber';
12
12
  declare module 'react' {
13
13
  namespace JSX {
@@ -15,31 +15,8 @@ declare module 'react' {
15
15
  }
16
16
  }
17
17
  }
18
- export type { CityData, CityBuilding, CityDistrict };
19
- export interface HighlightLayer {
20
- /** Unique identifier */
21
- id: string;
22
- /** Display name */
23
- name: string;
24
- /** Whether layer is active */
25
- enabled: boolean;
26
- /** Highlight color (hex) */
27
- color: string;
28
- /** Items to highlight */
29
- items: HighlightItem[];
30
- /** Opacity for highlighted items (0-1) */
31
- opacity?: number;
32
- /** Rendering priority (higher renders on top) */
33
- priority?: number;
34
- /** Border width in pixels */
35
- borderWidth?: number;
36
- }
37
- export interface HighlightItem {
38
- /** File or directory path */
39
- path: string;
40
- /** Type of item */
41
- type: 'file' | 'directory';
42
- }
18
+ export type { CityData, CityBuilding, CityDistrict, LayerItem, LayerRenderStrategy };
19
+ export type HighlightLayer = BuilderHighlightLayer;
43
20
  /** What to do with non-highlighted buildings */
44
21
  export type IsolationMode = 'none' | 'transparent' | 'collapse' | 'hide';
45
22
  export interface AnimationConfig {
@@ -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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"}
@@ -155,21 +155,48 @@ function getColorForFile(building) {
155
155
  return getConfigForFile(building).color;
156
156
  }
157
157
  /**
158
- * Check if a path is highlighted by any enabled layer.
158
+ * Get ALL layer matches for a path, sorted by priority (highest first).
159
+ * Returns array to support multiple layers rendering together (e.g., fill + border).
159
160
  */
160
- function getHighlightForPath(path, layers) {
161
+ function getLayerMatchesForPath(path, layers) {
162
+ const matches = [];
161
163
  for (const layer of layers) {
162
164
  if (!layer.enabled)
163
165
  continue;
164
166
  for (const item of layer.items) {
167
+ let isMatch = false;
165
168
  if (item.type === 'file' && item.path === path) {
166
- return { color: layer.color, opacity: layer.opacity ?? 1 };
169
+ isMatch = true;
170
+ }
171
+ else if (item.type === 'directory' && (path === item.path || path.startsWith(item.path + '/'))) {
172
+ isMatch = true;
167
173
  }
168
- if (item.type === 'directory' && (path === item.path || path.startsWith(item.path + '/'))) {
169
- return { color: layer.color, opacity: layer.opacity ?? 1 };
174
+ if (isMatch) {
175
+ matches.push({
176
+ layer,
177
+ item,
178
+ color: layer.color,
179
+ opacity: layer.opacity ?? 1,
180
+ borderWidth: layer.borderWidth,
181
+ renderStrategy: item.renderStrategy || 'border', // Default from 2D renderer
182
+ });
170
183
  }
171
184
  }
172
185
  }
186
+ // Sort by priority (highest first)
187
+ return matches.sort((a, b) => (b.layer.priority ?? 0) - (a.layer.priority ?? 0));
188
+ }
189
+ /**
190
+ * Get the highest-priority fill color for a path (backward compatibility).
191
+ * Returns the first matching layer with 'fill' strategy.
192
+ */
193
+ function getHighlightForPath(path, layers) {
194
+ const matches = getLayerMatchesForPath(path, layers);
195
+ // Find first fill match
196
+ const fillMatch = matches.find(m => m.renderStrategy === 'fill');
197
+ if (fillMatch) {
198
+ return { color: fillMatch.color, opacity: fillMatch.opacity };
199
+ }
173
200
  return null;
174
201
  }
175
202
  function hasActiveHighlights(layers) {
@@ -233,6 +260,177 @@ function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springD
233
260
  return null;
234
261
  return (_jsxs("instancedMesh", { ref: meshRef, args: [undefined, undefined, numEdges], frustumCulled: false, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshBasicMaterial", { color: "#1a1a2e", transparent: true, opacity: 0.7 })] }));
235
262
  }
263
+ function BorderHighlights({ buildings, centerOffset, highlightLayers, growProgress, minHeight, baseOffset, springDuration, heightMultipliersRef, heightScaling, linearScale, flatPatterns, staggerIndices, animationConfig, }) {
264
+ const meshRef = useRef(null);
265
+ const startTimeRef = useRef(null);
266
+ const tempObject = useMemo(() => new THREE.Object3D(), []);
267
+ const tempColor = useMemo(() => new THREE.Color(), []);
268
+ // Pre-compute border edge data from buildings with border highlights
269
+ const borderEdgeData = useMemo(() => {
270
+ const edges = [];
271
+ buildings.forEach((building, buildingIndex) => {
272
+ const matches = getLayerMatchesForPath(building.path, highlightLayers);
273
+ // Find border matches
274
+ const borderMatches = matches.filter(m => m.renderStrategy === 'border');
275
+ if (borderMatches.length === 0)
276
+ return;
277
+ // Use highest priority border match
278
+ const borderMatch = borderMatches[0];
279
+ const [width, , depth] = building.dimensions;
280
+ const fullHeight = calculateBuildingHeight(building, heightScaling, linearScale, flatPatterns);
281
+ const x = building.position.x - centerOffset.x;
282
+ const z = building.position.z - centerOffset.z;
283
+ const staggerIndex = staggerIndices[buildingIndex] ?? buildingIndex;
284
+ const staggerDelayMs = (animationConfig.staggerDelay || 15) * staggerIndex;
285
+ const halfW = width / 2;
286
+ const halfD = depth / 2;
287
+ // Create 4 vertical corner edges
288
+ const corners = [
289
+ { x: x - halfW, z: z - halfD },
290
+ { x: x + halfW, z: z - halfD },
291
+ { x: x - halfW, z: z + halfD },
292
+ { x: x + halfW, z: z + halfD },
293
+ ];
294
+ corners.forEach(corner => {
295
+ edges.push({
296
+ x: corner.x,
297
+ z: corner.z,
298
+ fullHeight,
299
+ buildingIndex,
300
+ staggerDelayMs,
301
+ color: borderMatch.color,
302
+ opacity: borderMatch.opacity,
303
+ borderWidth: borderMatch.borderWidth ?? 2,
304
+ edgeType: 'vertical',
305
+ });
306
+ });
307
+ // Create 4 horizontal edges on top (roof outline)
308
+ // Two edges along X axis (front and back)
309
+ edges.push({
310
+ x: x,
311
+ z: z - halfD,
312
+ fullHeight,
313
+ buildingIndex,
314
+ staggerDelayMs,
315
+ color: borderMatch.color,
316
+ opacity: borderMatch.opacity,
317
+ borderWidth: borderMatch.borderWidth ?? 2,
318
+ edgeType: 'horizontal-x',
319
+ width,
320
+ });
321
+ edges.push({
322
+ x: x,
323
+ z: z + halfD,
324
+ fullHeight,
325
+ buildingIndex,
326
+ staggerDelayMs,
327
+ color: borderMatch.color,
328
+ opacity: borderMatch.opacity,
329
+ borderWidth: borderMatch.borderWidth ?? 2,
330
+ edgeType: 'horizontal-x',
331
+ width,
332
+ });
333
+ // Two edges along Z axis (left and right)
334
+ edges.push({
335
+ x: x - halfW,
336
+ z: z,
337
+ fullHeight,
338
+ buildingIndex,
339
+ staggerDelayMs,
340
+ color: borderMatch.color,
341
+ opacity: borderMatch.opacity,
342
+ borderWidth: borderMatch.borderWidth ?? 2,
343
+ edgeType: 'horizontal-z',
344
+ depth,
345
+ });
346
+ edges.push({
347
+ x: x + halfW,
348
+ z: z,
349
+ fullHeight,
350
+ buildingIndex,
351
+ staggerDelayMs,
352
+ color: borderMatch.color,
353
+ opacity: borderMatch.opacity,
354
+ borderWidth: borderMatch.borderWidth ?? 2,
355
+ edgeType: 'horizontal-z',
356
+ depth,
357
+ });
358
+ });
359
+ return edges;
360
+ }, [
361
+ buildings,
362
+ centerOffset,
363
+ highlightLayers,
364
+ heightScaling,
365
+ linearScale,
366
+ flatPatterns,
367
+ staggerIndices,
368
+ animationConfig.staggerDelay,
369
+ ]);
370
+ // Animate border edges
371
+ useFrame(({ clock }) => {
372
+ if (!meshRef.current || borderEdgeData.length === 0)
373
+ return;
374
+ if (startTimeRef.current === null && growProgress > 0) {
375
+ startTimeRef.current = clock.elapsedTime * 1000;
376
+ }
377
+ const currentTime = clock.elapsedTime * 1000;
378
+ const animStartTime = startTimeRef.current ?? currentTime;
379
+ borderEdgeData.forEach((edge, idx) => {
380
+ const { x, z, fullHeight, staggerDelayMs, buildingIndex, color, opacity, borderWidth, edgeType, width, depth } = edge;
381
+ // Get height multiplier from shared ref (for collapse animation)
382
+ const heightMultiplier = heightMultipliersRef.current?.[buildingIndex] ?? 1;
383
+ // Calculate per-building animation progress
384
+ const elapsed = currentTime - animStartTime - staggerDelayMs;
385
+ let animProgress = growProgress;
386
+ if (growProgress > 0 && elapsed >= 0) {
387
+ const t = Math.min(elapsed / springDuration, 1);
388
+ const eased = 1 - Math.pow(1 - t, 3);
389
+ animProgress = eased * growProgress;
390
+ }
391
+ else if (growProgress > 0 && elapsed < 0) {
392
+ animProgress = 0;
393
+ }
394
+ // Apply both grow animation and collapse multiplier
395
+ const height = animProgress * fullHeight * heightMultiplier + minHeight;
396
+ // Fixed thickness based on borderWidth (don't scale with building size)
397
+ const thickness = Math.max(0.2, borderWidth * 0.1); // Convert pixels to world units
398
+ if (edgeType === 'vertical') {
399
+ // Vertical corner edges
400
+ const yPosition = height / 2 + baseOffset;
401
+ tempObject.position.set(x, yPosition, z);
402
+ tempObject.rotation.set(0, 0, 0);
403
+ tempObject.scale.set(thickness, height, thickness);
404
+ }
405
+ else if (edgeType === 'horizontal-x') {
406
+ // Horizontal edges along X axis (front/back of roof)
407
+ const yPosition = height + baseOffset;
408
+ tempObject.position.set(x, yPosition, z);
409
+ tempObject.rotation.set(0, 0, Math.PI / 2); // Rotate to horizontal along X
410
+ tempObject.scale.set(thickness, width, thickness);
411
+ }
412
+ else if (edgeType === 'horizontal-z') {
413
+ // Horizontal edges along Z axis (left/right of roof)
414
+ const yPosition = height + baseOffset;
415
+ tempObject.position.set(x, yPosition, z);
416
+ tempObject.rotation.set(Math.PI / 2, 0, 0); // Rotate to horizontal along Z
417
+ tempObject.scale.set(thickness, depth, thickness);
418
+ }
419
+ tempObject.updateMatrix();
420
+ meshRef.current.setMatrixAt(idx, tempObject.matrix);
421
+ // Set per-instance color with opacity
422
+ tempColor.set(color);
423
+ meshRef.current.setColorAt(idx, tempColor);
424
+ });
425
+ meshRef.current.instanceMatrix.needsUpdate = true;
426
+ if (meshRef.current.instanceColor) {
427
+ meshRef.current.instanceColor.needsUpdate = true;
428
+ }
429
+ });
430
+ if (borderEdgeData.length === 0)
431
+ return null;
432
+ return (_jsxs("instancedMesh", { ref: meshRef, args: [undefined, undefined, borderEdgeData.length], frustumCulled: false, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshBasicMaterial", { transparent: true, opacity: 0.9, vertexColors: true })] }));
433
+ }
236
434
  // Helper to check if a path is inside a directory
237
435
  function isPathInDirectory(path, directory) {
238
436
  if (!directory)
@@ -313,9 +511,10 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
313
511
  return buildings.map((building, index) => {
314
512
  const [width, , depth] = building.dimensions;
315
513
  const fullHeight = calculateBuildingHeight(building, heightScaling, linearScale, flatPatterns);
316
- // Use highlight layer color if available, otherwise fall back to file config color
317
- const highlight = getHighlightForPath(building.path, highlightLayers);
318
- const color = highlight?.color ?? getColorForFile(building);
514
+ // Get all layer matches and find first fill match for building color
515
+ const matches = getLayerMatchesForPath(building.path, highlightLayers);
516
+ const fillMatch = matches.find(m => m.renderStrategy === 'fill');
517
+ const color = fillMatch?.color ?? getColorForFile(building);
319
518
  const x = building.position.x - centerOffset.x;
320
519
  const z = building.position.z - centerOffset.z;
321
520
  const staggerIndex = staggerIndices[index] ?? index;
@@ -470,7 +669,7 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
470
669
  z: d.z,
471
670
  staggerDelayMs: d.staggerDelayMs,
472
671
  buildingIndex: d.index,
473
- })), growProgress: growProgress, minHeight: minHeight, baseOffset: baseOffset, springDuration: springDuration, heightMultipliersRef: heightMultipliersRef })] }));
672
+ })), 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 })] }));
474
673
  }
475
674
  function AnimatedIcon({ x, z, targetHeight, iconSize, texture, opacity, growProgress, }) {
476
675
  const meshRef = useRef(null);
@@ -2,5 +2,5 @@
2
2
  * FileCity3D - 3D visualization component
3
3
  */
4
4
  export { FileCity3D, resetCamera, getCameraAngle, getCameraTarget, getCameraTilt, rotateCameraTo, rotateCameraBy, tiltCameraTo, tiltCameraBy, moveCameraTo, setCameraTarget, DEFAULT_FLAT_PATTERNS, } from './FileCity3D';
5
- export type { FileCity3DProps, AnimationConfig, HighlightLayer, HighlightItem, IsolationMode, HeightScaling, FlatPattern, CityData, CityBuilding, CityDistrict, } from './FileCity3D';
5
+ export type { FileCity3DProps, AnimationConfig, HighlightLayer, LayerItem, LayerRenderStrategy, IsolationMode, HeightScaling, FlatPattern, CityData, CityBuilding, CityDistrict, } from './FileCity3D';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,UAAU,EACV,WAAW,EACX,cAAc,EACd,eAAe,EACf,aAAa,EACb,cAAc,EACd,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,qBAAqB,GACtB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,eAAe,EACf,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,EACb,aAAa,EACb,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,YAAY,GACb,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,UAAU,EACV,WAAW,EACX,cAAc,EACd,eAAe,EACf,aAAa,EACb,cAAc,EACd,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,qBAAqB,GACtB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,eAAe,EACf,eAAe,EACf,cAAc,EACd,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,aAAa,EACb,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,YAAY,GACb,MAAM,cAAc,CAAC"}
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export type { FileTree } from '@principal-ai/file-city-builder';
14
14
  export { CityViewWithReactFlow, type CityViewWithReactFlowProps, } from './components/CityViewWithReactFlow';
15
15
  export { ThemeProvider, useTheme } from '@principal-ade/industry-theme';
16
16
  export { FileCity3D, resetCamera, DEFAULT_FLAT_PATTERNS } from './components/FileCity3D';
17
- export type { FileCity3DProps, AnimationConfig, HighlightLayer as FileCity3DHighlightLayer, HighlightItem as FileCity3DHighlightItem, HighlightItem, IsolationMode, HeightScaling, FlatPattern, } from './components/FileCity3D';
17
+ export type { FileCity3DProps, AnimationConfig, HighlightLayer as FileCity3DHighlightLayer, IsolationMode, HeightScaling, FlatPattern, } from './components/FileCity3D';
18
18
  export type { HighlightLayer as FileCity3DHL } from './components/FileCity3D';
19
19
  export { resolveVisualizationIntent } from './utils/visualizationResolution';
20
20
  export type { VisualizationIntent, ResolvedVisualizationState, } from './utils/visualizationResolution';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,KAAK,mCAAmC,GACzC,MAAM,6CAA6C,CAAC;AAGrD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,UAAU,GACX,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EACL,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAGzF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG7F,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,QAAQ,EACR,UAAU,GACX,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAG7F,YAAY,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAGhE,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACzF,YAAY,EACV,eAAe,EACf,eAAe,EACf,cAAc,IAAI,wBAAwB,EAC1C,aAAa,IAAI,uBAAuB,EACxC,aAAa,EACb,aAAa,EACb,aAAa,EACb,WAAW,GACZ,MAAM,yBAAyB,CAAC;AAIjC,YAAY,EAAE,cAAc,IAAI,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAI9E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,YAAY,EACV,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,iCAAiC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,KAAK,mCAAmC,GACzC,MAAM,6CAA6C,CAAC;AAGrD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,UAAU,GACX,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EACL,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAGzF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG7F,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,QAAQ,EACR,UAAU,GACX,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAG7F,YAAY,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAGhE,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACzF,YAAY,EACV,eAAe,EACf,eAAe,EACf,cAAc,IAAI,wBAAwB,EAC1C,aAAa,EACb,aAAa,EACb,WAAW,GACZ,MAAM,yBAAyB,CAAC;AAIjC,YAAY,EAAE,cAAc,IAAI,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAI9E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,YAAY,EACV,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,iCAAiC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/file-city-react",
3
- "version": "0.5.35",
3
+ "version": "0.5.37",
4
4
  "type": "module",
5
5
  "description": "React components for File City visualization",
6
6
  "main": "dist/index.js",
@@ -18,6 +18,9 @@ import type {
18
18
  CityBuilding,
19
19
  CityDistrict,
20
20
  FileConfigResult,
21
+ HighlightLayer as BuilderHighlightLayer,
22
+ LayerItem,
23
+ LayerRenderStrategy,
21
24
  } from '@principal-ai/file-city-builder';
22
25
  import * as THREE from 'three';
23
26
  import type { ThreeElements } from '@react-three/fiber';
@@ -33,34 +36,8 @@ declare module 'react' {
33
36
  }
34
37
 
35
38
  // Re-export types for convenience
36
- export type { CityData, CityBuilding, CityDistrict };
37
-
38
- // Highlight layer types
39
- export interface HighlightLayer {
40
- /** Unique identifier */
41
- id: string;
42
- /** Display name */
43
- name: string;
44
- /** Whether layer is active */
45
- enabled: boolean;
46
- /** Highlight color (hex) */
47
- color: string;
48
- /** Items to highlight */
49
- items: HighlightItem[];
50
- /** Opacity for highlighted items (0-1) */
51
- opacity?: number;
52
- /** Rendering priority (higher renders on top) */
53
- priority?: number;
54
- /** Border width in pixels */
55
- borderWidth?: number;
56
- }
57
-
58
- export interface HighlightItem {
59
- /** File or directory path */
60
- path: string;
61
- /** Type of item */
62
- type: 'file' | 'directory';
63
- }
39
+ export type { CityData, CityBuilding, CityDistrict, LayerItem, LayerRenderStrategy };
40
+ export type HighlightLayer = BuilderHighlightLayer;
64
41
 
65
42
  /** What to do with non-highlighted buildings */
66
43
  export type IsolationMode =
@@ -273,25 +250,71 @@ function getColorForFile(building: CityBuilding): string {
273
250
  return getConfigForFile(building).color;
274
251
  }
275
252
 
253
+ interface LayerMatch {
254
+ layer: HighlightLayer;
255
+ item: LayerItem;
256
+ color: string;
257
+ opacity: number;
258
+ borderWidth?: number;
259
+ renderStrategy: LayerRenderStrategy;
260
+ }
261
+
276
262
  /**
277
- * Check if a path is highlighted by any enabled layer.
263
+ * Get ALL layer matches for a path, sorted by priority (highest first).
264
+ * Returns array to support multiple layers rendering together (e.g., fill + border).
278
265
  */
279
- function getHighlightForPath(
266
+ function getLayerMatchesForPath(
280
267
  path: string,
281
268
  layers: HighlightLayer[],
282
- ): { color: string; opacity: number } | null {
269
+ ): LayerMatch[] {
270
+ const matches: LayerMatch[] = [];
271
+
283
272
  for (const layer of layers) {
284
273
  if (!layer.enabled) continue;
285
274
 
286
275
  for (const item of layer.items) {
276
+ let isMatch = false;
277
+
287
278
  if (item.type === 'file' && item.path === path) {
288
- return { color: layer.color, opacity: layer.opacity ?? 1 };
279
+ isMatch = true;
280
+ } else if (item.type === 'directory' && (path === item.path || path.startsWith(item.path + '/'))) {
281
+ isMatch = true;
289
282
  }
290
- if (item.type === 'directory' && (path === item.path || path.startsWith(item.path + '/'))) {
291
- return { color: layer.color, opacity: layer.opacity ?? 1 };
283
+
284
+ if (isMatch) {
285
+ matches.push({
286
+ layer,
287
+ item,
288
+ color: layer.color,
289
+ opacity: layer.opacity ?? 1,
290
+ borderWidth: layer.borderWidth,
291
+ renderStrategy: item.renderStrategy || 'border', // Default from 2D renderer
292
+ });
292
293
  }
293
294
  }
294
295
  }
296
+
297
+ // Sort by priority (highest first)
298
+ return matches.sort((a, b) => (b.layer.priority ?? 0) - (a.layer.priority ?? 0));
299
+ }
300
+
301
+ /**
302
+ * Get the highest-priority fill color for a path (backward compatibility).
303
+ * Returns the first matching layer with 'fill' strategy.
304
+ */
305
+ function getHighlightForPath(
306
+ path: string,
307
+ layers: HighlightLayer[],
308
+ ): { color: string; opacity: number } | null {
309
+ const matches = getLayerMatchesForPath(path, layers);
310
+
311
+ // Find first fill match
312
+ const fillMatch = matches.find(m => m.renderStrategy === 'fill');
313
+
314
+ if (fillMatch) {
315
+ return { color: fillMatch.color, opacity: fillMatch.opacity };
316
+ }
317
+
295
318
  return null;
296
319
  }
297
320
 
@@ -406,6 +429,252 @@ function BuildingEdges({
406
429
  );
407
430
  }
408
431
 
432
+ // ============================================================================
433
+ // Border Highlights - Colored edge outlines for highlighted buildings
434
+ // ============================================================================
435
+
436
+ interface BorderEdgeData {
437
+ x: number;
438
+ z: number;
439
+ fullHeight: number;
440
+ buildingIndex: number;
441
+ staggerDelayMs: number;
442
+ color: string;
443
+ opacity: number;
444
+ borderWidth: number;
445
+ edgeType: 'vertical' | 'horizontal-x' | 'horizontal-z'; // Edge orientation
446
+ width?: number; // For horizontal edges (length along X axis)
447
+ depth?: number; // For horizontal edges (length along Z axis)
448
+ }
449
+
450
+ interface BorderHighlightsProps {
451
+ buildings: CityBuilding[];
452
+ centerOffset: { x: number; z: number };
453
+ highlightLayers: HighlightLayer[];
454
+ growProgress: number;
455
+ minHeight: number;
456
+ baseOffset: number;
457
+ springDuration: number;
458
+ heightMultipliersRef: React.MutableRefObject<Float32Array | null>;
459
+ heightScaling: HeightScaling;
460
+ linearScale: number;
461
+ flatPatterns: FlatPattern[];
462
+ staggerIndices: number[];
463
+ animationConfig: AnimationConfig;
464
+ }
465
+
466
+ function BorderHighlights({
467
+ buildings,
468
+ centerOffset,
469
+ highlightLayers,
470
+ growProgress,
471
+ minHeight,
472
+ baseOffset,
473
+ springDuration,
474
+ heightMultipliersRef,
475
+ heightScaling,
476
+ linearScale,
477
+ flatPatterns,
478
+ staggerIndices,
479
+ animationConfig,
480
+ }: BorderHighlightsProps) {
481
+ const meshRef = useRef<THREE.InstancedMesh>(null);
482
+ const startTimeRef = useRef<number | null>(null);
483
+ const tempObject = useMemo(() => new THREE.Object3D(), []);
484
+ const tempColor = useMemo(() => new THREE.Color(), []);
485
+
486
+ // Pre-compute border edge data from buildings with border highlights
487
+ const borderEdgeData = useMemo(() => {
488
+ const edges: BorderEdgeData[] = [];
489
+
490
+ buildings.forEach((building, buildingIndex) => {
491
+ const matches = getLayerMatchesForPath(building.path, highlightLayers);
492
+
493
+ // Find border matches
494
+ const borderMatches = matches.filter(m => m.renderStrategy === 'border');
495
+
496
+ if (borderMatches.length === 0) return;
497
+
498
+ // Use highest priority border match
499
+ const borderMatch = borderMatches[0];
500
+
501
+ const [width, , depth] = building.dimensions;
502
+ const fullHeight = calculateBuildingHeight(building, heightScaling, linearScale, flatPatterns);
503
+ const x = building.position.x - centerOffset.x;
504
+ const z = building.position.z - centerOffset.z;
505
+ const staggerIndex = staggerIndices[buildingIndex] ?? buildingIndex;
506
+ const staggerDelayMs = (animationConfig.staggerDelay || 15) * staggerIndex;
507
+
508
+ const halfW = width / 2;
509
+ const halfD = depth / 2;
510
+
511
+ // Create 4 vertical corner edges
512
+ const corners = [
513
+ { x: x - halfW, z: z - halfD },
514
+ { x: x + halfW, z: z - halfD },
515
+ { x: x - halfW, z: z + halfD },
516
+ { x: x + halfW, z: z + halfD },
517
+ ];
518
+
519
+ corners.forEach(corner => {
520
+ edges.push({
521
+ x: corner.x,
522
+ z: corner.z,
523
+ fullHeight,
524
+ buildingIndex,
525
+ staggerDelayMs,
526
+ color: borderMatch.color,
527
+ opacity: borderMatch.opacity,
528
+ borderWidth: borderMatch.borderWidth ?? 2,
529
+ edgeType: 'vertical',
530
+ });
531
+ });
532
+
533
+ // Create 4 horizontal edges on top (roof outline)
534
+ // Two edges along X axis (front and back)
535
+ edges.push({
536
+ x: x,
537
+ z: z - halfD,
538
+ fullHeight,
539
+ buildingIndex,
540
+ staggerDelayMs,
541
+ color: borderMatch.color,
542
+ opacity: borderMatch.opacity,
543
+ borderWidth: borderMatch.borderWidth ?? 2,
544
+ edgeType: 'horizontal-x',
545
+ width,
546
+ });
547
+ edges.push({
548
+ x: x,
549
+ z: z + halfD,
550
+ fullHeight,
551
+ buildingIndex,
552
+ staggerDelayMs,
553
+ color: borderMatch.color,
554
+ opacity: borderMatch.opacity,
555
+ borderWidth: borderMatch.borderWidth ?? 2,
556
+ edgeType: 'horizontal-x',
557
+ width,
558
+ });
559
+
560
+ // Two edges along Z axis (left and right)
561
+ edges.push({
562
+ x: x - halfW,
563
+ z: z,
564
+ fullHeight,
565
+ buildingIndex,
566
+ staggerDelayMs,
567
+ color: borderMatch.color,
568
+ opacity: borderMatch.opacity,
569
+ borderWidth: borderMatch.borderWidth ?? 2,
570
+ edgeType: 'horizontal-z',
571
+ depth,
572
+ });
573
+ edges.push({
574
+ x: x + halfW,
575
+ z: z,
576
+ fullHeight,
577
+ buildingIndex,
578
+ staggerDelayMs,
579
+ color: borderMatch.color,
580
+ opacity: borderMatch.opacity,
581
+ borderWidth: borderMatch.borderWidth ?? 2,
582
+ edgeType: 'horizontal-z',
583
+ depth,
584
+ });
585
+ });
586
+
587
+ return edges;
588
+ }, [
589
+ buildings,
590
+ centerOffset,
591
+ highlightLayers,
592
+ heightScaling,
593
+ linearScale,
594
+ flatPatterns,
595
+ staggerIndices,
596
+ animationConfig.staggerDelay,
597
+ ]);
598
+
599
+ // Animate border edges
600
+ useFrame(({ clock }) => {
601
+ if (!meshRef.current || borderEdgeData.length === 0) return;
602
+
603
+ if (startTimeRef.current === null && growProgress > 0) {
604
+ startTimeRef.current = clock.elapsedTime * 1000;
605
+ }
606
+
607
+ const currentTime = clock.elapsedTime * 1000;
608
+ const animStartTime = startTimeRef.current ?? currentTime;
609
+
610
+ borderEdgeData.forEach((edge, idx) => {
611
+ const { x, z, fullHeight, staggerDelayMs, buildingIndex, color, opacity, borderWidth, edgeType, width, depth } = edge;
612
+
613
+ // Get height multiplier from shared ref (for collapse animation)
614
+ const heightMultiplier = heightMultipliersRef.current?.[buildingIndex] ?? 1;
615
+
616
+ // Calculate per-building animation progress
617
+ const elapsed = currentTime - animStartTime - staggerDelayMs;
618
+ let animProgress = growProgress;
619
+
620
+ if (growProgress > 0 && elapsed >= 0) {
621
+ const t = Math.min(elapsed / springDuration, 1);
622
+ const eased = 1 - Math.pow(1 - t, 3);
623
+ animProgress = eased * growProgress;
624
+ } else if (growProgress > 0 && elapsed < 0) {
625
+ animProgress = 0;
626
+ }
627
+
628
+ // Apply both grow animation and collapse multiplier
629
+ const height = animProgress * fullHeight * heightMultiplier + minHeight;
630
+
631
+ // Fixed thickness based on borderWidth (don't scale with building size)
632
+ const thickness = Math.max(0.2, borderWidth * 0.1); // Convert pixels to world units
633
+
634
+ if (edgeType === 'vertical') {
635
+ // Vertical corner edges
636
+ const yPosition = height / 2 + baseOffset;
637
+ tempObject.position.set(x, yPosition, z);
638
+ tempObject.rotation.set(0, 0, 0);
639
+ tempObject.scale.set(thickness, height, thickness);
640
+ } else if (edgeType === 'horizontal-x') {
641
+ // Horizontal edges along X axis (front/back of roof)
642
+ const yPosition = height + baseOffset;
643
+ tempObject.position.set(x, yPosition, z);
644
+ tempObject.rotation.set(0, 0, Math.PI / 2); // Rotate to horizontal along X
645
+ tempObject.scale.set(thickness, width!, thickness);
646
+ } else if (edgeType === 'horizontal-z') {
647
+ // Horizontal edges along Z axis (left/right of roof)
648
+ const yPosition = height + baseOffset;
649
+ tempObject.position.set(x, yPosition, z);
650
+ tempObject.rotation.set(Math.PI / 2, 0, 0); // Rotate to horizontal along Z
651
+ tempObject.scale.set(thickness, depth!, thickness);
652
+ }
653
+
654
+ tempObject.updateMatrix();
655
+ meshRef.current!.setMatrixAt(idx, tempObject.matrix);
656
+
657
+ // Set per-instance color with opacity
658
+ tempColor.set(color);
659
+ meshRef.current!.setColorAt(idx, tempColor);
660
+ });
661
+
662
+ meshRef.current.instanceMatrix.needsUpdate = true;
663
+ if (meshRef.current.instanceColor) {
664
+ meshRef.current.instanceColor.needsUpdate = true;
665
+ }
666
+ });
667
+
668
+ if (borderEdgeData.length === 0) return null;
669
+
670
+ return (
671
+ <instancedMesh ref={meshRef} args={[undefined, undefined, borderEdgeData.length]} frustumCulled={false}>
672
+ <boxGeometry args={[1, 1, 1]} />
673
+ <meshBasicMaterial transparent opacity={0.9} vertexColors />
674
+ </instancedMesh>
675
+ );
676
+ }
677
+
409
678
  // ============================================================================
410
679
  // Instanced Buildings - High performance rendering for large scenes
411
680
  // ============================================================================
@@ -532,9 +801,10 @@ function InstancedBuildings({
532
801
  return buildings.map((building, index) => {
533
802
  const [width, , depth] = building.dimensions;
534
803
  const fullHeight = calculateBuildingHeight(building, heightScaling, linearScale, flatPatterns);
535
- // Use highlight layer color if available, otherwise fall back to file config color
536
- const highlight = getHighlightForPath(building.path, highlightLayers);
537
- const color = highlight?.color ?? getColorForFile(building);
804
+ // Get all layer matches and find first fill match for building color
805
+ const matches = getLayerMatchesForPath(building.path, highlightLayers);
806
+ const fillMatch = matches.find(m => m.renderStrategy === 'fill');
807
+ const color = fillMatch?.color ?? getColorForFile(building);
538
808
 
539
809
  const x = building.position.x - centerOffset.x;
540
810
  const z = building.position.z - centerOffset.z;
@@ -751,6 +1021,23 @@ function InstancedBuildings({
751
1021
  springDuration={springDuration}
752
1022
  heightMultipliersRef={heightMultipliersRef}
753
1023
  />
1024
+
1025
+ {/* Border highlights (colored, layer-driven) */}
1026
+ <BorderHighlights
1027
+ buildings={buildings}
1028
+ centerOffset={centerOffset}
1029
+ highlightLayers={highlightLayers}
1030
+ growProgress={growProgress}
1031
+ minHeight={minHeight}
1032
+ baseOffset={baseOffset}
1033
+ springDuration={springDuration}
1034
+ heightMultipliersRef={heightMultipliersRef}
1035
+ heightScaling={heightScaling}
1036
+ linearScale={linearScale}
1037
+ flatPatterns={flatPatterns}
1038
+ staggerIndices={staggerIndices}
1039
+ animationConfig={animationConfig}
1040
+ />
754
1041
  </group>
755
1042
  );
756
1043
  }
@@ -20,7 +20,8 @@ export type {
20
20
  FileCity3DProps,
21
21
  AnimationConfig,
22
22
  HighlightLayer,
23
- HighlightItem,
23
+ LayerItem,
24
+ LayerRenderStrategy,
24
25
  IsolationMode,
25
26
  HeightScaling,
26
27
  FlatPattern,
package/src/index.ts CHANGED
@@ -77,8 +77,6 @@ export type {
77
77
  FileCity3DProps,
78
78
  AnimationConfig,
79
79
  HighlightLayer as FileCity3DHighlightLayer,
80
- HighlightItem as FileCity3DHighlightItem,
81
- HighlightItem,
82
80
  IsolationMode,
83
81
  HeightScaling,
84
82
  FlatPattern,
@@ -201,7 +201,7 @@ export const SideBySide: StoryObj = {
201
201
  fontWeight: 500,
202
202
  }}
203
203
  >
204
- 3D FLAT
204
+ 3D FLAT (with test border)
205
205
  </div>
206
206
  <div style={{ flex: 1, backgroundColor: '#0f1419' }}>
207
207
  <FileCity3D
@@ -210,7 +210,7 @@ export const SideBySide: StoryObj = {
210
210
  width="100%"
211
211
  height="100%"
212
212
  isGrown={false}
213
- showControls={false}
213
+ showControls={true}
214
214
  backgroundColor="#0f1419"
215
215
  />
216
216
  </div>
@@ -422,7 +422,7 @@ export const IsolationTransparent: Story = {
422
422
  name: 'Focus Layer',
423
423
  enabled: true,
424
424
  color: '#22c55e',
425
- items: [{ path: 'src', type: 'directory' as const }],
425
+ items: [{ path: 'src', type: 'directory' as const, renderStrategy: 'fill' as const }],
426
426
  },
427
427
  ],
428
428
  },
@@ -442,7 +442,7 @@ export const IsolationCollapse: Story = {
442
442
  name: 'Focus Layer',
443
443
  enabled: true,
444
444
  color: '#3b82f6',
445
- items: [{ path: 'src/components', type: 'directory' as const }],
445
+ items: [{ path: 'src/components', type: 'directory' as const, renderStrategy: 'fill' as const }],
446
446
  },
447
447
  ],
448
448
  },
@@ -462,7 +462,7 @@ export const IsolationHide: Story = {
462
462
  name: 'Focus Layer',
463
463
  enabled: true,
464
464
  color: '#f59e0b',
465
- items: [{ path: 'tests', type: 'directory' as const }],
465
+ items: [{ path: 'tests', type: 'directory' as const, renderStrategy: 'fill' as const }],
466
466
  },
467
467
  ],
468
468
  },
@@ -483,14 +483,73 @@ export const MultipleHighlights: Story = {
483
483
  name: 'Source',
484
484
  enabled: true,
485
485
  color: '#22c55e',
486
- items: [{ path: 'src', type: 'directory' as const }],
486
+ items: [{ path: 'src', type: 'directory' as const, renderStrategy: 'fill' as const }],
487
487
  },
488
488
  {
489
489
  id: 'tests',
490
490
  name: 'Tests',
491
491
  enabled: true,
492
492
  color: '#ef4444',
493
- items: [{ path: 'tests', type: 'directory' as const }],
493
+ items: [{ path: 'tests', type: 'directory' as const, renderStrategy: 'fill' as const }],
494
+ },
495
+ ],
496
+ },
497
+ };
498
+
499
+ /**
500
+ * Layered Highlights - Multiple overlapping highlight layers
501
+ * Tests priority-based rendering with fill + border strategies
502
+ * Starts flat (2D) - click "Grow" button to see in 3D
503
+ */
504
+ export const LayeredHighlights: Story = {
505
+ args: {
506
+ cityData: sampleCityData,
507
+ height: '100vh',
508
+ isolationMode: 'transparent',
509
+ dimOpacity: 0.05,
510
+ animation: {
511
+ startFlat: true,
512
+ autoStartDelay: null, // Stay in 2D, use button to grow
513
+ },
514
+ showControls: true,
515
+ highlightLayers: [
516
+ {
517
+ id: 'base-fill',
518
+ name: 'Base Fill (src)',
519
+ enabled: true,
520
+ color: '#3b82f6',
521
+ opacity: 0.7,
522
+ priority: 1,
523
+ items: [{ path: 'src', type: 'directory' as const, renderStrategy: 'fill' as const }],
524
+ },
525
+ {
526
+ id: 'components-fill',
527
+ name: 'Components Fill (src/components)',
528
+ enabled: true,
529
+ color: '#facc15',
530
+ opacity: 1.0,
531
+ priority: 2,
532
+ items: [{ path: 'src/components', type: 'directory' as const, renderStrategy: 'fill' as const }],
533
+ },
534
+ {
535
+ id: 'components-border',
536
+ name: 'Components Border (src/components)',
537
+ enabled: true,
538
+ color: '#ef4444',
539
+ opacity: 1.0,
540
+ priority: 3,
541
+ borderWidth: 6,
542
+ items: [{ path: 'src/components', type: 'directory' as const, renderStrategy: 'border' as const }],
543
+ },
544
+ {
545
+ id: 'utils-border',
546
+ name: 'Utils Border (src/utils)',
547
+ enabled: true,
548
+ color: '#22c55e',
549
+ opacity: 1.0,
550
+ priority: 4,
551
+ borderWidth: 4,
552
+ items: [{ path: 'src/utils', type: 'directory' as const, renderStrategy: 'border' as const }],
494
553
  },
495
554
  ],
496
555
  },
@@ -516,7 +575,7 @@ export const AnimatedWithHighlight: Story = {
516
575
  name: 'Components',
517
576
  enabled: true,
518
577
  color: '#8b5cf6',
519
- items: [{ path: 'src/components', type: 'directory' as const }],
578
+ items: [{ path: 'src/components', type: 'directory' as const, renderStrategy: 'fill' as const }],
520
579
  },
521
580
  ],
522
581
  },