Files
XCSDD/src/components/blueprint/SystemStructure.tsx

312 lines
8.3 KiB
TypeScript

import { useRef, useMemo } from 'react';
import { useFrame } from '@react-three/fiber';
import { Text, Billboard, QuadraticBezierLine } from '@react-three/drei';
import * as THREE from 'three';
import { useBlueprintStore } from '../../store/blueprintStore';
const NODE_COLOR = '#aaaaaa';
const ROOT_COLOR = '#ffffff';
function splitCamelCase(str: string): string[] {
if (str === str.toUpperCase()) {
return [str];
}
const result: string[] = [];
let current = '';
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (char >= 'A' && char <= 'Z' && current.length > 0) {
result.push(current);
current = char;
} else {
current += char;
}
}
if (current) result.push(current);
return result;
}
function SystemNode({
id,
name,
position,
isRoot = false,
size = 0.8
}: {
id: string;
name: string;
position: [number, number, number];
isRoot?: boolean;
size?: number;
}) {
const meshRef = useRef<THREE.Mesh>(null);
const { selectedNode, setSelectedNode } = useBlueprintStore();
const isSelected = selectedNode === id;
const color = isRoot ? ROOT_COLOR : NODE_COLOR;
useFrame(() => {
if (meshRef.current) {
meshRef.current.rotation.y += 0.005;
}
});
return (
<group position={position}>
<mesh
ref={meshRef}
onClick={(e) => {
e.stopPropagation();
setSelectedNode(id);
}}
onPointerOver={() => document.body.style.cursor = 'pointer'}
onPointerOut={() => document.body.style.cursor = 'auto'}
>
<sphereGeometry args={[size, 32, 32]} />
<meshStandardMaterial
color={color}
emissive={color}
emissiveIntensity={isSelected ? 0.8 : 0.3}
metalness={0.5}
roughness={0.3}
/>
</mesh>
<Billboard follow={true}>
{(() => {
const parts = splitCamelCase(name);
const lineHeight = 0.45;
const startY = size + 0.5 + (parts.length - 1) * lineHeight / 2;
return (
<>
{parts.map((part, i) => (
<Text
key={i}
position={[0, startY - i * lineHeight, 0]}
fontSize={0.35}
color="#ffffff"
anchorX="center"
anchorY="middle"
>
{part}
</Text>
))}
</>
);
})()}
</Billboard>
{isSelected && (
<mesh>
<sphereGeometry args={[size + 0.1, 32, 32]} />
<meshBasicMaterial color={color} transparent opacity={0.3} />
</mesh>
)}
</group>
);
}
function fibonacciSpherePoint(i: number, n: number, radius: number): [number, number, number] {
const phi = Math.acos(1 - 2 * (i + 0.5) / n);
const theta = Math.PI * (1 + Math.sqrt(5)) * i;
return [
radius * Math.sin(phi) * Math.cos(theta),
radius * Math.cos(phi),
radius * Math.sin(phi) * Math.sin(theta),
];
}
function ConnectionLine({
start,
end,
color = '#4a5568',
lineWidth = 1.5
}: {
start: [number, number, number];
end: [number, number, number];
color?: string;
lineWidth?: number;
}) {
const dx = end[0] - start[0];
const dy = end[1] - start[1];
const dz = end[2] - start[2];
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
const midX = (start[0] + end[0]) / 2;
const midY = (start[1] + end[1]) / 2;
const midZ = (start[2] + end[2]) / 2;
const controlOffset = dist * 0.25;
const yDirection = dy > 0 ? 1 : -1;
return (
<QuadraticBezierLine
start={start}
end={end}
mid={[
midX - dz * controlOffset / dist,
midY + yDirection * controlOffset,
midZ + dx * controlOffset / dist,
]}
color={color}
lineWidth={lineWidth}
transparent
opacity={0.6}
/>
);
}
export default function SystemStructure() {
const { blueprint, selectedNode } = useBlueprintStore();
const { subsystems, modules } = blueprint;
const rootPosition: [number, number, number] = [0, 0, 0];
const subsystemPositions = useMemo(() => {
const radius = 8;
const count = subsystems.length;
return subsystems.map((sub, i) => {
const position = fibonacciSpherePoint(i, count, radius);
position[1] *= 0.6;
return {
id: sub.id,
position: position as [number, number, number],
dependCount: sub.depends_on.length,
};
});
}, [subsystems]);
const getSubsystemSize = (dependCount: number) => {
const baseSize = 0.6;
const maxAdd = 0.4;
return baseSize + Math.min(dependCount * 0.15, maxAdd);
};
const modulePositions = useMemo(() => {
const positions: { id: string; name: string; position: [number, number, number]; parentId: string }[] = [];
const moduleRadius = 2.5;
subsystems.forEach((sub) => {
const subModules = modules.filter(m => m.parent_subsystem === sub.id);
const subPos = subsystemPositions.find(p => p.id === sub.id);
if (!subPos) return;
const count = subModules.length || 1;
subModules.forEach((mod, i) => {
const offset = fibonacciSpherePoint(i, count, moduleRadius);
const parentY = subPos.position[1];
const yOffset = parentY > 0 ? 1.2 : -1.2;
positions.push({
id: mod.id,
name: mod.name,
position: [
subPos.position[0] + offset[0],
parentY + yOffset + offset[1] * 0.5,
subPos.position[2] + offset[2],
] as [number, number, number],
parentId: sub.id,
});
});
});
return positions;
}, [subsystems, modules, subsystemPositions]);
const dependencies = useMemo(() => {
const lines: { from: [number, number, number]; to: [number, number, number]; fromId: string; toId: string }[] = [];
subsystems.forEach(sub => {
const fromPos = subsystemPositions.find(p => p.id === sub.id);
if (!fromPos) return;
sub.depends_on.forEach(depId => {
const toPos = subsystemPositions.find(p => p.id === depId);
if (toPos) {
lines.push({
from: fromPos.position,
to: toPos.position,
fromId: sub.id,
toId: depId,
});
}
});
});
return lines;
}, [subsystems, subsystemPositions]);
return (
<group>
<SystemNode
id="root"
name={blueprint.meta.name}
position={rootPosition}
isRoot
size={1.2}
/>
{subsystemPositions.map((sub) => (
<SystemNode
key={sub.id}
id={sub.id}
name={sub.id}
position={sub.position}
size={getSubsystemSize(sub.dependCount)}
/>
))}
{modulePositions.map((mod) => (
<SystemNode
key={mod.id}
id={mod.id}
name={mod.name}
position={mod.position}
size={0.4}
/>
))}
{dependencies.map((dep, i) => {
const isHighlighted = selectedNode && (dep.fromId === selectedNode || dep.toId === selectedNode);
return (
<ConnectionLine
key={i}
start={dep.from}
end={dep.to}
color={isHighlighted ? '#ffffff' : '#666666'}
lineWidth={isHighlighted ? 3 : 1.5}
/>
);
})}
{subsystemPositions.map((sub) => {
const isHighlighted = selectedNode === sub.id || selectedNode === 'root';
return (
<ConnectionLine
key={`root-${sub.id}`}
start={rootPosition}
end={sub.position}
color={isHighlighted ? '#ffffff' : '#666666'}
lineWidth={isHighlighted ? 3 : 1.5}
/>
);
})}
{modulePositions.map((mod) => {
const parent = subsystemPositions.find(p => p.id === mod.parentId);
if (!parent) return null;
const isHighlighted = selectedNode === mod.id || selectedNode === mod.parentId;
return (
<ConnectionLine
key={`mod-${mod.id}`}
start={parent.position}
end={mod.position}
color={isHighlighted ? '#ffffff' : '#666666'}
lineWidth={isHighlighted ? 3 : 1.5}
/>
);
})}
</group>
);
}