@principal-ai/file-city-react 0.5.7 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAMN,MAAM,OAAO,CAAC;AAWf,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAEb,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxD,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,CAAC;AAGrD,MAAM,WAAW,cAAc;IAC7B,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;CAC5B;AAED,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;
|
|
1
|
+
{"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"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"}
|
|
@@ -95,6 +95,66 @@ function calculateBuildingHeight(building, scaling = 'logarithmic', linearScale
|
|
|
95
95
|
// Fallback to dimension height if no metrics available
|
|
96
96
|
return building.dimensions[1];
|
|
97
97
|
}
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Icon Texture Generation - Lucide icon SVG paths
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Lucide icon paths (from lucide.dev)
|
|
102
|
+
const LUCIDE_ICONS = {
|
|
103
|
+
Atom: '<circle cx="12" cy="12" r="1"/><path d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"/><path d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"/>',
|
|
104
|
+
Lock: '<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
|
105
|
+
EyeOff: '<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/>',
|
|
106
|
+
Key: '<path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/>',
|
|
107
|
+
GitBranch: '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
|
|
108
|
+
TestTube: '<path d="M14.5 2v17.5c0 1.4-1.1 2.5-2.5 2.5c-1.4 0-2.5-1.1-2.5-2.5V2"/><path d="M8.5 2h7"/><path d="M14.5 16h-5"/>',
|
|
109
|
+
FlaskConical: '<path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2"/><path d="M8.5 2h7"/><path d="M7 16h10"/>',
|
|
110
|
+
BookText: '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/><path d="M8 11h8"/><path d="M8 7h6"/>',
|
|
111
|
+
BookOpen: '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
|
|
112
|
+
ScrollText: '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
|
|
113
|
+
Settings: '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
|
|
114
|
+
Home: '<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
|
115
|
+
};
|
|
116
|
+
// Cache for icon textures
|
|
117
|
+
const iconTextureCache = new Map();
|
|
118
|
+
/**
|
|
119
|
+
* Generate a texture from a Lucide icon
|
|
120
|
+
*/
|
|
121
|
+
function getIconTexture(iconName, color = '#ffffff') {
|
|
122
|
+
const cacheKey = `${iconName}-${color}`;
|
|
123
|
+
if (iconTextureCache.has(cacheKey)) {
|
|
124
|
+
return iconTextureCache.get(cacheKey);
|
|
125
|
+
}
|
|
126
|
+
const iconPath = LUCIDE_ICONS[iconName];
|
|
127
|
+
if (!iconPath) {
|
|
128
|
+
// Icon not in our subset, skip silently
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">${iconPath}</svg>`;
|
|
132
|
+
// Create canvas and draw SVG
|
|
133
|
+
const canvas = document.createElement('canvas');
|
|
134
|
+
canvas.width = 128;
|
|
135
|
+
canvas.height = 128;
|
|
136
|
+
const ctx = canvas.getContext('2d');
|
|
137
|
+
if (!ctx)
|
|
138
|
+
return null;
|
|
139
|
+
// Create image from SVG
|
|
140
|
+
const img = new Image();
|
|
141
|
+
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
142
|
+
const url = URL.createObjectURL(svgBlob);
|
|
143
|
+
// Create texture (will update when image loads)
|
|
144
|
+
const texture = new THREE.Texture(canvas);
|
|
145
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
146
|
+
img.onload = () => {
|
|
147
|
+
// Clear canvas with transparent background
|
|
148
|
+
ctx.clearRect(0, 0, 128, 128);
|
|
149
|
+
// Draw centered icon
|
|
150
|
+
ctx.drawImage(img, 32, 32, 64, 64);
|
|
151
|
+
texture.needsUpdate = true;
|
|
152
|
+
URL.revokeObjectURL(url);
|
|
153
|
+
};
|
|
154
|
+
img.src = url;
|
|
155
|
+
iconTextureCache.set(cacheKey, texture);
|
|
156
|
+
return texture;
|
|
157
|
+
}
|
|
98
158
|
// Get full file config from centralized file-city-builder lookup
|
|
99
159
|
function getConfigForFile(building) {
|
|
100
160
|
if (building.color) {
|
|
@@ -135,6 +195,61 @@ function hasActiveHighlights(layers) {
|
|
|
135
195
|
}
|
|
136
196
|
// Animated RoundedBox wrapper
|
|
137
197
|
const AnimatedRoundedBox = animated(RoundedBox);
|
|
198
|
+
function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springDuration }) {
|
|
199
|
+
const meshRef = useRef(null);
|
|
200
|
+
const startTimeRef = useRef(null);
|
|
201
|
+
const tempObject = useMemo(() => new THREE.Object3D(), []);
|
|
202
|
+
// 4 corner edges per building
|
|
203
|
+
const numEdges = buildings.length * 4;
|
|
204
|
+
// Pre-compute edge data
|
|
205
|
+
const edgeData = useMemo(() => {
|
|
206
|
+
return buildings.flatMap((data) => {
|
|
207
|
+
const { width, depth, x, z, targetHeight, staggerDelayMs } = data;
|
|
208
|
+
const halfW = width / 2;
|
|
209
|
+
const halfD = depth / 2;
|
|
210
|
+
return [
|
|
211
|
+
{ x: x - halfW, z: z - halfD, targetHeight, staggerDelayMs },
|
|
212
|
+
{ x: x + halfW, z: z - halfD, targetHeight, staggerDelayMs },
|
|
213
|
+
{ x: x - halfW, z: z + halfD, targetHeight, staggerDelayMs },
|
|
214
|
+
{ x: x + halfW, z: z + halfD, targetHeight, staggerDelayMs },
|
|
215
|
+
];
|
|
216
|
+
});
|
|
217
|
+
}, [buildings]);
|
|
218
|
+
// Animate edges
|
|
219
|
+
useFrame(({ clock }) => {
|
|
220
|
+
if (!meshRef.current || edgeData.length === 0)
|
|
221
|
+
return;
|
|
222
|
+
if (startTimeRef.current === null && growProgress > 0) {
|
|
223
|
+
startTimeRef.current = clock.elapsedTime * 1000;
|
|
224
|
+
}
|
|
225
|
+
const currentTime = clock.elapsedTime * 1000;
|
|
226
|
+
const animStartTime = startTimeRef.current ?? currentTime;
|
|
227
|
+
edgeData.forEach((edge, idx) => {
|
|
228
|
+
const { x, z, targetHeight, staggerDelayMs } = edge;
|
|
229
|
+
// Calculate per-building animation progress
|
|
230
|
+
const elapsed = currentTime - animStartTime - staggerDelayMs;
|
|
231
|
+
let animProgress = growProgress;
|
|
232
|
+
if (growProgress > 0 && elapsed >= 0) {
|
|
233
|
+
const t = Math.min(elapsed / springDuration, 1);
|
|
234
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
235
|
+
animProgress = eased * growProgress;
|
|
236
|
+
}
|
|
237
|
+
else if (growProgress > 0 && elapsed < 0) {
|
|
238
|
+
animProgress = 0;
|
|
239
|
+
}
|
|
240
|
+
const height = animProgress * targetHeight + minHeight;
|
|
241
|
+
const yPosition = height / 2 + baseOffset;
|
|
242
|
+
tempObject.position.set(x, yPosition, z);
|
|
243
|
+
tempObject.scale.set(0.3, height, 0.3); // Thin box for edge
|
|
244
|
+
tempObject.updateMatrix();
|
|
245
|
+
meshRef.current.setMatrixAt(idx, tempObject.matrix);
|
|
246
|
+
});
|
|
247
|
+
meshRef.current.instanceMatrix.needsUpdate = true;
|
|
248
|
+
});
|
|
249
|
+
if (numEdges === 0)
|
|
250
|
+
return null;
|
|
251
|
+
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 })] }));
|
|
252
|
+
}
|
|
138
253
|
function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hoveredIndex, growProgress, animationConfig, highlightLayers, isolationMode, hasActiveHighlights, dimOpacity, heightScaling, linearScale, staggerIndices, }) {
|
|
139
254
|
const meshRef = useRef(null);
|
|
140
255
|
const startTimeRef = useRef(null);
|
|
@@ -279,7 +394,92 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
|
|
|
279
394
|
}, [visibleBuildings, onClick]);
|
|
280
395
|
if (visibleBuildings.length === 0)
|
|
281
396
|
return null;
|
|
282
|
-
return (_jsxs("instancedMesh", { ref: meshRef, args: [undefined, undefined, visibleBuildings.length], onPointerMove: handlePointerMove, onPointerOut: handlePointerOut, onClick: handleClick, frustumCulled: false, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { metalness: 0.1, roughness: 0.35 })] }));
|
|
397
|
+
return (_jsxs("group", { children: [_jsxs("instancedMesh", { ref: meshRef, args: [undefined, undefined, visibleBuildings.length], onPointerMove: handlePointerMove, onPointerOut: handlePointerOut, onClick: handleClick, frustumCulled: false, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { metalness: 0.1, roughness: 0.35 })] }), _jsx(BuildingEdges, { buildings: visibleBuildings, growProgress: growProgress, minHeight: minHeight, baseOffset: baseOffset, springDuration: springDuration })] }));
|
|
398
|
+
}
|
|
399
|
+
function AnimatedIcon({ x, z, targetHeight, iconSize, texture, opacity, growProgress, staggerDelayMs, springDuration, }) {
|
|
400
|
+
const spriteRef = useRef(null);
|
|
401
|
+
const startTimeRef = useRef(null);
|
|
402
|
+
const materialRef = useRef(null);
|
|
403
|
+
useFrame(({ clock }) => {
|
|
404
|
+
if (!spriteRef.current)
|
|
405
|
+
return;
|
|
406
|
+
if (startTimeRef.current === null && growProgress > 0) {
|
|
407
|
+
startTimeRef.current = clock.elapsedTime * 1000;
|
|
408
|
+
}
|
|
409
|
+
const currentTime = clock.elapsedTime * 1000;
|
|
410
|
+
const animStartTime = startTimeRef.current ?? currentTime;
|
|
411
|
+
// Calculate per-icon animation progress
|
|
412
|
+
const elapsed = currentTime - animStartTime - staggerDelayMs;
|
|
413
|
+
let animProgress = growProgress;
|
|
414
|
+
if (growProgress > 0 && elapsed >= 0) {
|
|
415
|
+
const t = Math.min(elapsed / springDuration, 1);
|
|
416
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
417
|
+
animProgress = eased * growProgress;
|
|
418
|
+
}
|
|
419
|
+
else if (growProgress > 0 && elapsed < 0) {
|
|
420
|
+
animProgress = 0;
|
|
421
|
+
}
|
|
422
|
+
const minHeight = 0.3;
|
|
423
|
+
const baseOffset = 0.2;
|
|
424
|
+
const height = animProgress * targetHeight + minHeight;
|
|
425
|
+
const buildingTop = height + baseOffset;
|
|
426
|
+
const yPosition = buildingTop + iconSize / 2 + 2;
|
|
427
|
+
spriteRef.current.position.y = yPosition;
|
|
428
|
+
if (materialRef.current) {
|
|
429
|
+
materialRef.current.opacity = opacity * animProgress;
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
return (_jsx("sprite", { ref: spriteRef, position: [x, 0, z], scale: [iconSize, iconSize, 1], children: _jsx("spriteMaterial", { ref: materialRef, map: texture, transparent: true, opacity: 0, depthTest: true, depthWrite: false }) }));
|
|
433
|
+
}
|
|
434
|
+
function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, linearScale, highlightLayers, isolationMode, hasActiveHighlights, staggerIndices, springDuration, staggerDelay, }) {
|
|
435
|
+
// Pre-compute buildings with icons
|
|
436
|
+
const buildingsWithIcons = useMemo(() => {
|
|
437
|
+
return buildings
|
|
438
|
+
.map((building, index) => {
|
|
439
|
+
const config = getConfigForFile(building);
|
|
440
|
+
if (!config.icon)
|
|
441
|
+
return null;
|
|
442
|
+
const highlight = getHighlightForPath(building.path, highlightLayers);
|
|
443
|
+
const isHighlighted = highlight !== null;
|
|
444
|
+
const shouldDim = hasActiveHighlights && !isHighlighted;
|
|
445
|
+
const shouldHide = shouldDim && isolationMode === 'hide';
|
|
446
|
+
const shouldCollapse = shouldDim && isolationMode === 'collapse';
|
|
447
|
+
if (shouldHide)
|
|
448
|
+
return null;
|
|
449
|
+
const fullHeight = calculateBuildingHeight(building, heightScaling, linearScale);
|
|
450
|
+
const targetHeight = shouldCollapse ? 0.5 : fullHeight;
|
|
451
|
+
const x = building.position.x - centerOffset.x;
|
|
452
|
+
const z = building.position.z - centerOffset.z;
|
|
453
|
+
const staggerIndex = staggerIndices[index] ?? index;
|
|
454
|
+
const staggerDelayMs = staggerDelay * staggerIndex;
|
|
455
|
+
return {
|
|
456
|
+
building,
|
|
457
|
+
config,
|
|
458
|
+
x,
|
|
459
|
+
z,
|
|
460
|
+
targetHeight,
|
|
461
|
+
shouldDim,
|
|
462
|
+
staggerDelayMs,
|
|
463
|
+
};
|
|
464
|
+
})
|
|
465
|
+
.filter(Boolean);
|
|
466
|
+
}, [buildings, centerOffset, highlightLayers, isolationMode, hasActiveHighlights, heightScaling, linearScale, staggerIndices, staggerDelay]);
|
|
467
|
+
// Don't render if no progress yet
|
|
468
|
+
if (growProgress < 0.1)
|
|
469
|
+
return null;
|
|
470
|
+
return (_jsx(_Fragment, { children: buildingsWithIcons.map(({ building, config, x, z, targetHeight, shouldDim, staggerDelayMs }) => {
|
|
471
|
+
const icon = config.icon;
|
|
472
|
+
const texture = getIconTexture(icon.name, icon.color || '#ffffff');
|
|
473
|
+
if (!texture)
|
|
474
|
+
return null;
|
|
475
|
+
// Icon size based on building dimensions
|
|
476
|
+
const [width] = building.dimensions;
|
|
477
|
+
const baseSize = Math.max(width * 0.8, 6);
|
|
478
|
+
const heightBoost = Math.min(targetHeight / 20, 3);
|
|
479
|
+
const iconSize = (baseSize + heightBoost) * (icon.size || 1);
|
|
480
|
+
const opacity = shouldDim && isolationMode === 'transparent' ? 0.3 : 1;
|
|
481
|
+
return (_jsx(AnimatedIcon, { x: x, z: z, targetHeight: targetHeight, iconSize: iconSize, texture: texture, opacity: opacity, growProgress: growProgress, staggerDelayMs: staggerDelayMs, springDuration: springDuration }, building.path));
|
|
482
|
+
}) }));
|
|
283
483
|
}
|
|
284
484
|
function DistrictFloor({ district, centerOffset, opacity, }) {
|
|
285
485
|
const { worldBounds } = district;
|
|
@@ -398,7 +598,11 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
|
|
|
398
598
|
return null;
|
|
399
599
|
return cityData.buildings.findIndex((b) => b.path === hoveredBuilding.path);
|
|
400
600
|
}, [hoveredBuilding, cityData.buildings]);
|
|
401
|
-
|
|
601
|
+
// Calculate spring duration for animation sync
|
|
602
|
+
const tension = animationConfig.tension || 120;
|
|
603
|
+
const friction = animationConfig.friction || 14;
|
|
604
|
+
const springDuration = Math.sqrt(1 / (tension * 0.001)) * friction * 20;
|
|
605
|
+
return (_jsxs(_Fragment, { children: [_jsx(AnimatedCamera, { citySize: citySize, isFlat: growProgress === 0 }), _jsx("ambientLight", { intensity: 1.2 }), _jsx("hemisphereLight", { args: ['#ddeeff', '#667788', 0.8], position: [0, citySize, 0] }), _jsx("directionalLight", { position: [citySize, citySize * 1.5, citySize * 0.5], intensity: 2, castShadow: true, "shadow-mapSize": [2048, 2048] }), _jsx("directionalLight", { position: [-citySize * 0.5, citySize * 0.8, -citySize * 0.5], intensity: 1 }), _jsx("directionalLight", { position: [citySize * 0.3, citySize, citySize], intensity: 0.6 }), cityData.districts.map((district) => (_jsx(DistrictFloor, { district: district, centerOffset: centerOffset, opacity: 1 }, district.path))), _jsx(InstancedBuildings, { buildings: cityData.buildings, centerOffset: centerOffset, onHover: onBuildingHover, onClick: onBuildingClick, hoveredIndex: hoveredIndex, growProgress: growProgress, animationConfig: animationConfig, highlightLayers: highlightLayers, isolationMode: isolationMode, hasActiveHighlights: activeHighlights, dimOpacity: dimOpacity, heightScaling: heightScaling, linearScale: linearScale, staggerIndices: staggerIndices }), _jsx(BuildingIcons, { buildings: cityData.buildings, centerOffset: centerOffset, growProgress: growProgress, heightScaling: heightScaling, linearScale: linearScale, highlightLayers: highlightLayers, isolationMode: isolationMode, hasActiveHighlights: activeHighlights, staggerIndices: staggerIndices, springDuration: springDuration, staggerDelay: animationConfig.staggerDelay || 15 })] }));
|
|
402
606
|
}
|
|
403
607
|
/**
|
|
404
608
|
* FileCity3D - 3D visualization of codebase structure
|
package/package.json
CHANGED
|
@@ -186,6 +186,80 @@ function calculateBuildingHeight(
|
|
|
186
186
|
return building.dimensions[1];
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// ============================================================================
|
|
190
|
+
// Icon Texture Generation - Lucide icon SVG paths
|
|
191
|
+
// ============================================================================
|
|
192
|
+
|
|
193
|
+
// Lucide icon paths (from lucide.dev)
|
|
194
|
+
const LUCIDE_ICONS: Record<string, string> = {
|
|
195
|
+
Atom: '<circle cx="12" cy="12" r="1"/><path d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"/><path d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"/>',
|
|
196
|
+
Lock: '<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
|
197
|
+
EyeOff: '<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/>',
|
|
198
|
+
Key: '<path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/>',
|
|
199
|
+
GitBranch: '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
|
|
200
|
+
TestTube: '<path d="M14.5 2v17.5c0 1.4-1.1 2.5-2.5 2.5c-1.4 0-2.5-1.1-2.5-2.5V2"/><path d="M8.5 2h7"/><path d="M14.5 16h-5"/>',
|
|
201
|
+
FlaskConical: '<path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2"/><path d="M8.5 2h7"/><path d="M7 16h10"/>',
|
|
202
|
+
BookText: '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/><path d="M8 11h8"/><path d="M8 7h6"/>',
|
|
203
|
+
BookOpen: '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
|
|
204
|
+
ScrollText: '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
|
|
205
|
+
Settings: '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
|
|
206
|
+
Home: '<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Cache for icon textures
|
|
210
|
+
const iconTextureCache = new Map<string, THREE.Texture>();
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Generate a texture from a Lucide icon
|
|
214
|
+
*/
|
|
215
|
+
function getIconTexture(iconName: string, color: string = '#ffffff'): THREE.Texture | null {
|
|
216
|
+
const cacheKey = `${iconName}-${color}`;
|
|
217
|
+
|
|
218
|
+
if (iconTextureCache.has(cacheKey)) {
|
|
219
|
+
return iconTextureCache.get(cacheKey)!;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const iconPath = LUCIDE_ICONS[iconName];
|
|
223
|
+
if (!iconPath) {
|
|
224
|
+
// Icon not in our subset, skip silently
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">${iconPath}</svg>`;
|
|
229
|
+
|
|
230
|
+
// Create canvas and draw SVG
|
|
231
|
+
const canvas = document.createElement('canvas');
|
|
232
|
+
canvas.width = 128;
|
|
233
|
+
canvas.height = 128;
|
|
234
|
+
const ctx = canvas.getContext('2d');
|
|
235
|
+
if (!ctx) return null;
|
|
236
|
+
|
|
237
|
+
// Create image from SVG
|
|
238
|
+
const img = new Image();
|
|
239
|
+
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
240
|
+
const url = URL.createObjectURL(svgBlob);
|
|
241
|
+
|
|
242
|
+
// Create texture (will update when image loads)
|
|
243
|
+
const texture = new THREE.Texture(canvas);
|
|
244
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
245
|
+
|
|
246
|
+
img.onload = () => {
|
|
247
|
+
// Clear canvas with transparent background
|
|
248
|
+
ctx.clearRect(0, 0, 128, 128);
|
|
249
|
+
|
|
250
|
+
// Draw centered icon
|
|
251
|
+
ctx.drawImage(img, 32, 32, 64, 64);
|
|
252
|
+
|
|
253
|
+
texture.needsUpdate = true;
|
|
254
|
+
URL.revokeObjectURL(url);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
img.src = url;
|
|
258
|
+
|
|
259
|
+
iconTextureCache.set(cacheKey, texture);
|
|
260
|
+
return texture;
|
|
261
|
+
}
|
|
262
|
+
|
|
189
263
|
// Get full file config from centralized file-city-builder lookup
|
|
190
264
|
function getConfigForFile(building: CityBuilding): FileConfigResult {
|
|
191
265
|
if (building.color) {
|
|
@@ -236,6 +310,100 @@ function hasActiveHighlights(layers: HighlightLayer[]): boolean {
|
|
|
236
310
|
// Animated RoundedBox wrapper
|
|
237
311
|
const AnimatedRoundedBox = animated(RoundedBox);
|
|
238
312
|
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// Building Edges - Batched edge rendering for performance
|
|
315
|
+
// ============================================================================
|
|
316
|
+
|
|
317
|
+
interface BuildingEdgeData {
|
|
318
|
+
width: number;
|
|
319
|
+
depth: number;
|
|
320
|
+
targetHeight: number;
|
|
321
|
+
x: number;
|
|
322
|
+
z: number;
|
|
323
|
+
staggerDelayMs: number;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
interface BuildingEdgesProps {
|
|
327
|
+
buildings: BuildingEdgeData[];
|
|
328
|
+
growProgress: number;
|
|
329
|
+
minHeight: number;
|
|
330
|
+
baseOffset: number;
|
|
331
|
+
springDuration: number;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springDuration }: BuildingEdgesProps) {
|
|
335
|
+
const meshRef = useRef<THREE.InstancedMesh>(null);
|
|
336
|
+
const startTimeRef = useRef<number | null>(null);
|
|
337
|
+
const tempObject = useMemo(() => new THREE.Object3D(), []);
|
|
338
|
+
|
|
339
|
+
// 4 corner edges per building
|
|
340
|
+
const numEdges = buildings.length * 4;
|
|
341
|
+
|
|
342
|
+
// Pre-compute edge data
|
|
343
|
+
const edgeData = useMemo(() => {
|
|
344
|
+
return buildings.flatMap((data) => {
|
|
345
|
+
const { width, depth, x, z, targetHeight, staggerDelayMs } = data;
|
|
346
|
+
const halfW = width / 2;
|
|
347
|
+
const halfD = depth / 2;
|
|
348
|
+
|
|
349
|
+
return [
|
|
350
|
+
{ x: x - halfW, z: z - halfD, targetHeight, staggerDelayMs },
|
|
351
|
+
{ x: x + halfW, z: z - halfD, targetHeight, staggerDelayMs },
|
|
352
|
+
{ x: x - halfW, z: z + halfD, targetHeight, staggerDelayMs },
|
|
353
|
+
{ x: x + halfW, z: z + halfD, targetHeight, staggerDelayMs },
|
|
354
|
+
];
|
|
355
|
+
});
|
|
356
|
+
}, [buildings]);
|
|
357
|
+
|
|
358
|
+
// Animate edges
|
|
359
|
+
useFrame(({ clock }) => {
|
|
360
|
+
if (!meshRef.current || edgeData.length === 0) return;
|
|
361
|
+
|
|
362
|
+
if (startTimeRef.current === null && growProgress > 0) {
|
|
363
|
+
startTimeRef.current = clock.elapsedTime * 1000;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const currentTime = clock.elapsedTime * 1000;
|
|
367
|
+
const animStartTime = startTimeRef.current ?? currentTime;
|
|
368
|
+
|
|
369
|
+
edgeData.forEach((edge, idx) => {
|
|
370
|
+
const { x, z, targetHeight, staggerDelayMs } = edge;
|
|
371
|
+
|
|
372
|
+
// Calculate per-building animation progress
|
|
373
|
+
const elapsed = currentTime - animStartTime - staggerDelayMs;
|
|
374
|
+
let animProgress = growProgress;
|
|
375
|
+
|
|
376
|
+
if (growProgress > 0 && elapsed >= 0) {
|
|
377
|
+
const t = Math.min(elapsed / springDuration, 1);
|
|
378
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
379
|
+
animProgress = eased * growProgress;
|
|
380
|
+
} else if (growProgress > 0 && elapsed < 0) {
|
|
381
|
+
animProgress = 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const height = animProgress * targetHeight + minHeight;
|
|
385
|
+
const yPosition = height / 2 + baseOffset;
|
|
386
|
+
|
|
387
|
+
tempObject.position.set(x, yPosition, z);
|
|
388
|
+
tempObject.scale.set(0.3, height, 0.3); // Thin box for edge
|
|
389
|
+
tempObject.updateMatrix();
|
|
390
|
+
|
|
391
|
+
meshRef.current!.setMatrixAt(idx, tempObject.matrix);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
meshRef.current.instanceMatrix.needsUpdate = true;
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (numEdges === 0) return null;
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<instancedMesh ref={meshRef} args={[undefined, undefined, numEdges]} frustumCulled={false}>
|
|
401
|
+
<boxGeometry args={[1, 1, 1]} />
|
|
402
|
+
<meshBasicMaterial color="#1a1a2e" transparent opacity={0.7} />
|
|
403
|
+
</instancedMesh>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
239
407
|
// ============================================================================
|
|
240
408
|
// Instanced Buildings - High performance rendering for large scenes
|
|
241
409
|
// ============================================================================
|
|
@@ -465,17 +633,223 @@ function InstancedBuildings({
|
|
|
465
633
|
if (visibleBuildings.length === 0) return null;
|
|
466
634
|
|
|
467
635
|
return (
|
|
468
|
-
<
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
636
|
+
<group>
|
|
637
|
+
{/* Main building meshes */}
|
|
638
|
+
<instancedMesh
|
|
639
|
+
ref={meshRef}
|
|
640
|
+
args={[undefined, undefined, visibleBuildings.length]}
|
|
641
|
+
onPointerMove={handlePointerMove}
|
|
642
|
+
onPointerOut={handlePointerOut}
|
|
643
|
+
onClick={handleClick}
|
|
644
|
+
frustumCulled={false}
|
|
645
|
+
>
|
|
646
|
+
<boxGeometry args={[1, 1, 1]} />
|
|
647
|
+
<meshStandardMaterial metalness={0.1} roughness={0.35} />
|
|
648
|
+
</instancedMesh>
|
|
649
|
+
|
|
650
|
+
{/* Building edge outlines - batched into single geometry for performance */}
|
|
651
|
+
<BuildingEdges
|
|
652
|
+
buildings={visibleBuildings}
|
|
653
|
+
growProgress={growProgress}
|
|
654
|
+
minHeight={minHeight}
|
|
655
|
+
baseOffset={baseOffset}
|
|
656
|
+
springDuration={springDuration}
|
|
657
|
+
/>
|
|
658
|
+
</group>
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ============================================================================
|
|
663
|
+
// Building Icons - Renders icons on top of buildings
|
|
664
|
+
// ============================================================================
|
|
665
|
+
|
|
666
|
+
interface BuildingIconsProps {
|
|
667
|
+
buildings: CityBuilding[];
|
|
668
|
+
centerOffset: { x: number; z: number };
|
|
669
|
+
growProgress: number;
|
|
670
|
+
heightScaling: HeightScaling;
|
|
671
|
+
linearScale: number;
|
|
672
|
+
highlightLayers: HighlightLayer[];
|
|
673
|
+
isolationMode: IsolationMode;
|
|
674
|
+
hasActiveHighlights: boolean;
|
|
675
|
+
staggerIndices: number[];
|
|
676
|
+
springDuration: number;
|
|
677
|
+
staggerDelay: number;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Individual animated icon component
|
|
681
|
+
interface AnimatedIconProps {
|
|
682
|
+
x: number;
|
|
683
|
+
z: number;
|
|
684
|
+
targetHeight: number;
|
|
685
|
+
iconSize: number;
|
|
686
|
+
texture: THREE.Texture;
|
|
687
|
+
opacity: number;
|
|
688
|
+
growProgress: number;
|
|
689
|
+
staggerDelayMs: number;
|
|
690
|
+
springDuration: number;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function AnimatedIcon({
|
|
694
|
+
x,
|
|
695
|
+
z,
|
|
696
|
+
targetHeight,
|
|
697
|
+
iconSize,
|
|
698
|
+
texture,
|
|
699
|
+
opacity,
|
|
700
|
+
growProgress,
|
|
701
|
+
staggerDelayMs,
|
|
702
|
+
springDuration,
|
|
703
|
+
}: AnimatedIconProps) {
|
|
704
|
+
const spriteRef = useRef<THREE.Sprite>(null);
|
|
705
|
+
const startTimeRef = useRef<number | null>(null);
|
|
706
|
+
const materialRef = useRef<THREE.SpriteMaterial>(null);
|
|
707
|
+
|
|
708
|
+
useFrame(({ clock }) => {
|
|
709
|
+
if (!spriteRef.current) return;
|
|
710
|
+
|
|
711
|
+
if (startTimeRef.current === null && growProgress > 0) {
|
|
712
|
+
startTimeRef.current = clock.elapsedTime * 1000;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const currentTime = clock.elapsedTime * 1000;
|
|
716
|
+
const animStartTime = startTimeRef.current ?? currentTime;
|
|
717
|
+
|
|
718
|
+
// Calculate per-icon animation progress
|
|
719
|
+
const elapsed = currentTime - animStartTime - staggerDelayMs;
|
|
720
|
+
let animProgress = growProgress;
|
|
721
|
+
|
|
722
|
+
if (growProgress > 0 && elapsed >= 0) {
|
|
723
|
+
const t = Math.min(elapsed / springDuration, 1);
|
|
724
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
725
|
+
animProgress = eased * growProgress;
|
|
726
|
+
} else if (growProgress > 0 && elapsed < 0) {
|
|
727
|
+
animProgress = 0;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const minHeight = 0.3;
|
|
731
|
+
const baseOffset = 0.2;
|
|
732
|
+
const height = animProgress * targetHeight + minHeight;
|
|
733
|
+
const buildingTop = height + baseOffset;
|
|
734
|
+
const yPosition = buildingTop + iconSize / 2 + 2;
|
|
735
|
+
|
|
736
|
+
spriteRef.current.position.y = yPosition;
|
|
737
|
+
|
|
738
|
+
if (materialRef.current) {
|
|
739
|
+
materialRef.current.opacity = opacity * animProgress;
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
return (
|
|
744
|
+
<sprite
|
|
745
|
+
ref={spriteRef}
|
|
746
|
+
position={[x, 0, z]}
|
|
747
|
+
scale={[iconSize, iconSize, 1]}
|
|
475
748
|
>
|
|
476
|
-
<
|
|
477
|
-
|
|
478
|
-
|
|
749
|
+
<spriteMaterial
|
|
750
|
+
ref={materialRef}
|
|
751
|
+
map={texture}
|
|
752
|
+
transparent
|
|
753
|
+
opacity={0}
|
|
754
|
+
depthTest={true}
|
|
755
|
+
depthWrite={false}
|
|
756
|
+
/>
|
|
757
|
+
</sprite>
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function BuildingIcons({
|
|
762
|
+
buildings,
|
|
763
|
+
centerOffset,
|
|
764
|
+
growProgress,
|
|
765
|
+
heightScaling,
|
|
766
|
+
linearScale,
|
|
767
|
+
highlightLayers,
|
|
768
|
+
isolationMode,
|
|
769
|
+
hasActiveHighlights,
|
|
770
|
+
staggerIndices,
|
|
771
|
+
springDuration,
|
|
772
|
+
staggerDelay,
|
|
773
|
+
}: BuildingIconsProps) {
|
|
774
|
+
// Pre-compute buildings with icons
|
|
775
|
+
const buildingsWithIcons = useMemo(() => {
|
|
776
|
+
return buildings
|
|
777
|
+
.map((building, index) => {
|
|
778
|
+
const config = getConfigForFile(building);
|
|
779
|
+
if (!config.icon) return null;
|
|
780
|
+
|
|
781
|
+
const highlight = getHighlightForPath(building.path, highlightLayers);
|
|
782
|
+
const isHighlighted = highlight !== null;
|
|
783
|
+
const shouldDim = hasActiveHighlights && !isHighlighted;
|
|
784
|
+
const shouldHide = shouldDim && isolationMode === 'hide';
|
|
785
|
+
const shouldCollapse = shouldDim && isolationMode === 'collapse';
|
|
786
|
+
|
|
787
|
+
if (shouldHide) return null;
|
|
788
|
+
|
|
789
|
+
const fullHeight = calculateBuildingHeight(building, heightScaling, linearScale);
|
|
790
|
+
const targetHeight = shouldCollapse ? 0.5 : fullHeight;
|
|
791
|
+
|
|
792
|
+
const x = building.position.x - centerOffset.x;
|
|
793
|
+
const z = building.position.z - centerOffset.z;
|
|
794
|
+
|
|
795
|
+
const staggerIndex = staggerIndices[index] ?? index;
|
|
796
|
+
const staggerDelayMs = staggerDelay * staggerIndex;
|
|
797
|
+
|
|
798
|
+
return {
|
|
799
|
+
building,
|
|
800
|
+
config,
|
|
801
|
+
x,
|
|
802
|
+
z,
|
|
803
|
+
targetHeight,
|
|
804
|
+
shouldDim,
|
|
805
|
+
staggerDelayMs,
|
|
806
|
+
};
|
|
807
|
+
})
|
|
808
|
+
.filter(Boolean) as Array<{
|
|
809
|
+
building: CityBuilding;
|
|
810
|
+
config: FileConfigResult;
|
|
811
|
+
x: number;
|
|
812
|
+
z: number;
|
|
813
|
+
targetHeight: number;
|
|
814
|
+
shouldDim: boolean;
|
|
815
|
+
staggerDelayMs: number;
|
|
816
|
+
}>;
|
|
817
|
+
}, [buildings, centerOffset, highlightLayers, isolationMode, hasActiveHighlights, heightScaling, linearScale, staggerIndices, staggerDelay]);
|
|
818
|
+
|
|
819
|
+
// Don't render if no progress yet
|
|
820
|
+
if (growProgress < 0.1) return null;
|
|
821
|
+
|
|
822
|
+
return (
|
|
823
|
+
<>
|
|
824
|
+
{buildingsWithIcons.map(({ building, config, x, z, targetHeight, shouldDim, staggerDelayMs }) => {
|
|
825
|
+
const icon = config.icon!;
|
|
826
|
+
const texture = getIconTexture(icon.name, icon.color || '#ffffff');
|
|
827
|
+
if (!texture) return null;
|
|
828
|
+
|
|
829
|
+
// Icon size based on building dimensions
|
|
830
|
+
const [width] = building.dimensions;
|
|
831
|
+
const baseSize = Math.max(width * 0.8, 6);
|
|
832
|
+
const heightBoost = Math.min(targetHeight / 20, 3);
|
|
833
|
+
const iconSize = (baseSize + heightBoost) * (icon.size || 1);
|
|
834
|
+
|
|
835
|
+
const opacity = shouldDim && isolationMode === 'transparent' ? 0.3 : 1;
|
|
836
|
+
|
|
837
|
+
return (
|
|
838
|
+
<AnimatedIcon
|
|
839
|
+
key={building.path}
|
|
840
|
+
x={x}
|
|
841
|
+
z={z}
|
|
842
|
+
targetHeight={targetHeight}
|
|
843
|
+
iconSize={iconSize}
|
|
844
|
+
texture={texture}
|
|
845
|
+
opacity={opacity}
|
|
846
|
+
growProgress={growProgress}
|
|
847
|
+
staggerDelayMs={staggerDelayMs}
|
|
848
|
+
springDuration={springDuration}
|
|
849
|
+
/>
|
|
850
|
+
);
|
|
851
|
+
})}
|
|
852
|
+
</>
|
|
479
853
|
);
|
|
480
854
|
}
|
|
481
855
|
|
|
@@ -762,20 +1136,33 @@ function CityScene({
|
|
|
762
1136
|
return cityData.buildings.findIndex((b) => b.path === hoveredBuilding.path);
|
|
763
1137
|
}, [hoveredBuilding, cityData.buildings]);
|
|
764
1138
|
|
|
1139
|
+
// Calculate spring duration for animation sync
|
|
1140
|
+
const tension = animationConfig.tension || 120;
|
|
1141
|
+
const friction = animationConfig.friction || 14;
|
|
1142
|
+
const springDuration = Math.sqrt(1 / (tension * 0.001)) * friction * 20;
|
|
1143
|
+
|
|
765
1144
|
return (
|
|
766
1145
|
<>
|
|
767
1146
|
<AnimatedCamera citySize={citySize} isFlat={growProgress === 0} />
|
|
768
1147
|
|
|
769
|
-
<ambientLight intensity={
|
|
1148
|
+
<ambientLight intensity={1.2} />
|
|
1149
|
+
<hemisphereLight
|
|
1150
|
+
args={['#ddeeff', '#667788', 0.8]}
|
|
1151
|
+
position={[0, citySize, 0]}
|
|
1152
|
+
/>
|
|
770
1153
|
<directionalLight
|
|
771
|
-
position={[citySize, citySize, citySize * 0.5]}
|
|
772
|
-
intensity={
|
|
1154
|
+
position={[citySize, citySize * 1.5, citySize * 0.5]}
|
|
1155
|
+
intensity={2}
|
|
773
1156
|
castShadow
|
|
774
1157
|
shadow-mapSize={[2048, 2048]}
|
|
775
1158
|
/>
|
|
776
1159
|
<directionalLight
|
|
777
|
-
position={[-citySize * 0.5, citySize * 0.
|
|
778
|
-
intensity={
|
|
1160
|
+
position={[-citySize * 0.5, citySize * 0.8, -citySize * 0.5]}
|
|
1161
|
+
intensity={1}
|
|
1162
|
+
/>
|
|
1163
|
+
<directionalLight
|
|
1164
|
+
position={[citySize * 0.3, citySize, citySize]}
|
|
1165
|
+
intensity={0.6}
|
|
779
1166
|
/>
|
|
780
1167
|
|
|
781
1168
|
{cityData.districts.map((district) => (
|
|
@@ -803,6 +1190,20 @@ function CityScene({
|
|
|
803
1190
|
linearScale={linearScale}
|
|
804
1191
|
staggerIndices={staggerIndices}
|
|
805
1192
|
/>
|
|
1193
|
+
|
|
1194
|
+
<BuildingIcons
|
|
1195
|
+
buildings={cityData.buildings}
|
|
1196
|
+
centerOffset={centerOffset}
|
|
1197
|
+
growProgress={growProgress}
|
|
1198
|
+
heightScaling={heightScaling}
|
|
1199
|
+
linearScale={linearScale}
|
|
1200
|
+
highlightLayers={highlightLayers}
|
|
1201
|
+
isolationMode={isolationMode}
|
|
1202
|
+
hasActiveHighlights={activeHighlights}
|
|
1203
|
+
staggerIndices={staggerIndices}
|
|
1204
|
+
springDuration={springDuration}
|
|
1205
|
+
staggerDelay={animationConfig.staggerDelay || 15}
|
|
1206
|
+
/>
|
|
806
1207
|
</>
|
|
807
1208
|
);
|
|
808
1209
|
}
|
|
@@ -478,3 +478,65 @@ export const LinearHeightScaling: Story = {
|
|
|
478
478
|
linearScale: 0.5,
|
|
479
479
|
},
|
|
480
480
|
};
|
|
481
|
+
|
|
482
|
+
// Real repository data from JSON files
|
|
483
|
+
import authServerCityData from '../../../../assets/auth-server-city-data.json';
|
|
484
|
+
import electronAppCityData from '../../../../assets/electron-app-city-data.json';
|
|
485
|
+
import thisRepoCityData from '../../../../assets/this-repo-city-data.json';
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Auth Server - Real repository data
|
|
489
|
+
*/
|
|
490
|
+
export const AuthServer: Story = {
|
|
491
|
+
args: {
|
|
492
|
+
cityData: authServerCityData as CityData,
|
|
493
|
+
height: '100vh',
|
|
494
|
+
heightScaling: 'linear',
|
|
495
|
+
linearScale: 0.5,
|
|
496
|
+
animation: {
|
|
497
|
+
startFlat: true,
|
|
498
|
+
autoStartDelay: 800,
|
|
499
|
+
staggerDelay: 15,
|
|
500
|
+
tension: 120,
|
|
501
|
+
friction: 14,
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Electron App - Real repository data (larger)
|
|
508
|
+
*/
|
|
509
|
+
export const ElectronApp: Story = {
|
|
510
|
+
args: {
|
|
511
|
+
cityData: electronAppCityData as CityData,
|
|
512
|
+
height: '100vh',
|
|
513
|
+
heightScaling: 'linear',
|
|
514
|
+
linearScale: 0.5,
|
|
515
|
+
animation: {
|
|
516
|
+
startFlat: true,
|
|
517
|
+
autoStartDelay: 600,
|
|
518
|
+
staggerDelay: 5,
|
|
519
|
+
tension: 150,
|
|
520
|
+
friction: 16,
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* This Repo - industry-themed-repository-composition-panels
|
|
527
|
+
*/
|
|
528
|
+
export const ThisRepo: Story = {
|
|
529
|
+
args: {
|
|
530
|
+
cityData: thisRepoCityData as CityData,
|
|
531
|
+
height: '100vh',
|
|
532
|
+
heightScaling: 'linear',
|
|
533
|
+
linearScale: 0.5,
|
|
534
|
+
animation: {
|
|
535
|
+
startFlat: true,
|
|
536
|
+
autoStartDelay: 700,
|
|
537
|
+
staggerDelay: 10,
|
|
538
|
+
tension: 130,
|
|
539
|
+
friction: 14,
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
};
|