@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.
- package/dist/components/FileCity3D/FileCity3D.d.ts +3 -26
- package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
- package/dist/components/FileCity3D/FileCity3D.js +208 -9
- package/dist/components/FileCity3D/index.d.ts +1 -1
- package/dist/components/FileCity3D/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/FileCity3D/FileCity3D.tsx +324 -37
- package/src/components/FileCity3D/index.ts +2 -1
- package/src/index.ts +0 -2
- package/src/stories/2D3DComparison.stories.tsx +2 -2
- package/src/stories/FileCity3D.stories.tsx +65 -6
|
@@ -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
|
|
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":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAA4D,MAAM,OAAO,CAAC;AAMjF,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,
|
|
1
|
+
{"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAA4D,MAAM,OAAO,CAAC;AAMjF,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAEZ,cAAc,IAAI,qBAAqB,EACvC,SAAS,EACT,mBAAmB,EACpB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,OAAO,QAAQ,OAAO,CAAC;IAErB,UAAU,GAAG,CAAC;QAEZ,UAAU,iBAAkB,SAAQ,aAAa;SAAG;KACrD;CACF;AAGD,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,mBAAmB,EAAE,CAAC;AACrF,MAAM,MAAM,cAAc,GAAG,qBAAqB,CAAC;AAEnD,gDAAgD;AAChD,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,aAAa,GACb,UAAU,GACV,MAAM,CAAC;AAGX,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wCAAwC;AACxC,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,QAAQ,CAAC;AAErD,oFAAoF;AACpF,MAAM,WAAW,WAAW;IAC1B,qDAAqD;IACrD,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,yDAAyD;AACzD,eAAO,MAAM,qBAAqB,EAAE,WAAW,EAS9C,CAAC;AA4sCF,MAAM,WAAW,aAAa;IAC5B,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAmBD,wBAAgB,WAAW,SAE1B;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,QAE/D;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,QAEvF;AAED;;GAEG;AACH,wBAAgB,eAAe;OA9BA,MAAM;OAAK,MAAM;OAAK,MAAM;SAgC1D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,gBAAgB,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,EAC9D,OAAO,CAAC,EAAE,aAAa,QAGxB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,QAEtE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,EAChD,OAAO,CAAC,EAAE,aAAa,QAGxB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,QAEpE;AAED,wBAAgB,iBAAiB;OAhFA,MAAM;OAAK,MAAM;OAAK,MAAM;SAkF5D;AAED;;;GAGG;AACH,wBAAgB,cAAc,kBAE7B;AAED;;;GAGG;AACH,wBAAgB,aAAa,kBAE5B;AAuhCD,MAAM,WAAW,eAAe;IAC9B,uCAAuC;IACvC,QAAQ,EAAE,QAAQ,CAAC;IACnB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,0CAA0C;IAC1C,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;IACnD,qBAAqB;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,8BAA8B;IAC9B,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,uCAAuC;IACvC,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC1C,0GAA0G;IAC1G,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kEAAkE;IAClE,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;IACnC,yEAAyE;IACzE,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uCAAuC;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2IAA2I;IAC3I,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,0DAA0D;IAC1D,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACvD,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACvC,4EAA4E;IAC5E,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC,kEAAkE;IAClE,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;CACpC;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,KAAc,EACd,MAAY,EACZ,eAAe,EACf,SAAS,EACT,KAAK,EACL,SAAS,EACT,OAAO,EAAE,eAAe,EACxB,YAAY,EACZ,YAAoB,EACpB,eAAe,EAAE,uBAAuB,EACxC,aAAa,EAAE,qBAAqB,EACpC,UAAU,EAAE,WAAkB,EAC9B,SAAiB,EACjB,cAAuC,EACvC,YAA4C,EAC5C,aAAwB,EACxB,WAAe,EACf,YAAoC,EACpC,cAAc,EAAE,sBAAsB,EACtC,UAAU,EAAE,kBAAkB,EAC9B,iBAAiB,EAAE,kBAAkB,EACrC,eAA2B,EAC3B,SAAqB,EACrB,gBAAuB,EACvB,sBAA8B,EAC9B,eAAe,GAChB,EAAE,eAAe,2CAqKjB;AAED,eAAe,UAAU,CAAC"}
|
|
@@ -155,21 +155,48 @@ function getColorForFile(building) {
|
|
|
155
155
|
return getConfigForFile(building).color;
|
|
156
156
|
}
|
|
157
157
|
/**
|
|
158
|
-
*
|
|
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
|
|
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
|
-
|
|
169
|
+
isMatch = true;
|
|
170
|
+
}
|
|
171
|
+
else if (item.type === 'directory' && (path === item.path || path.startsWith(item.path + '/'))) {
|
|
172
|
+
isMatch = true;
|
|
167
173
|
}
|
|
168
|
-
if (
|
|
169
|
-
|
|
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
|
-
//
|
|
317
|
-
const
|
|
318
|
-
const
|
|
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,
|
|
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,
|
|
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,
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
@@ -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
|
-
*
|
|
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
|
|
266
|
+
function getLayerMatchesForPath(
|
|
280
267
|
path: string,
|
|
281
268
|
layers: HighlightLayer[],
|
|
282
|
-
):
|
|
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
|
-
|
|
279
|
+
isMatch = true;
|
|
280
|
+
} else if (item.type === 'directory' && (path === item.path || path.startsWith(item.path + '/'))) {
|
|
281
|
+
isMatch = true;
|
|
289
282
|
}
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
//
|
|
536
|
-
const
|
|
537
|
-
const
|
|
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
|
}
|
package/src/index.ts
CHANGED
|
@@ -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={
|
|
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
|
},
|