feat: merge XCBluePrint 3D visualization into project
This commit is contained in:
4424
package-lock.json
generated
4424
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@@ -4,25 +4,38 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-three/drei": "^10.7.7",
|
||||||
|
"@react-three/fiber": "^9.5.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
"react": "^18.3.1",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.2.4",
|
||||||
"tailwind-merge": "^3.5.0"
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"three": "^0.183.2",
|
||||||
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.3.12",
|
"@eslint/js": "^9.39.4",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@types/node": "^24.12.0",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
|
"@vitejs/plugin-react": "^6.0.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"playwright": "^1.58.2",
|
"eslint": "^9.39.4",
|
||||||
"postcss": "^8.5.3",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"tailwindcss": "^3.4.17",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"typescript": "~5.8.3",
|
"globals": "^17.4.0",
|
||||||
"vite": "^6.3.5"
|
"postcss": "^8.5.8",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.56.1",
|
||||||
|
"vite": "^8.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/App.tsx
51
src/App.tsx
@@ -1,9 +1,50 @@
|
|||||||
import { ApiDocViewer } from './components/ApiDocViewer'
|
import { useState } from 'react';
|
||||||
import { config } from './config'
|
import { FileText, Box } from 'lucide-react';
|
||||||
|
import { ApiDocViewer } from './components/ApiDocViewer';
|
||||||
|
import BlueprintPage from './components/blueprint/BlueprintPage';
|
||||||
|
import { config } from './config';
|
||||||
|
|
||||||
|
type Page = 'docs' | 'blueprint';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
document.title = config.projectName
|
const [currentPage, setCurrentPage] = useState<Page>('docs');
|
||||||
return <ApiDocViewer />
|
|
||||||
|
document.title = config.projectName;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex flex-col">
|
||||||
|
<header className="h-12 bg-zinc-900 border-b border-zinc-800 flex items-center px-4 justify-between">
|
||||||
|
<h1 className="text-sm font-medium text-white">{config.projectName}</h1>
|
||||||
|
<nav className="flex gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage('docs')}
|
||||||
|
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm ${
|
||||||
|
currentPage === 'docs'
|
||||||
|
? 'bg-zinc-800 text-white'
|
||||||
|
: 'text-zinc-400 hover:text-white hover:bg-zinc-800/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FileText size={16} />
|
||||||
|
API 文档
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage('blueprint')}
|
||||||
|
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm ${
|
||||||
|
currentPage === 'blueprint'
|
||||||
|
? 'bg-zinc-800 text-white'
|
||||||
|
: 'text-zinc-400 hover:text-white hover:bg-zinc-800/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Box size={16} />
|
||||||
|
3D 蓝图
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main className="flex-1 overflow-hidden">
|
||||||
|
{currentPage === 'docs' ? <ApiDocViewer /> : <BlueprintPage />}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { clsx } from 'clsx'
|
import clsx from 'clsx'
|
||||||
import type { ParsedDoc, DocTable, DocCodeBlock } from '@/lib/types'
|
import type { ParsedDoc, DocTable, DocCodeBlock } from '@/lib/types'
|
||||||
|
|
||||||
interface DocContentProps {
|
interface DocContentProps {
|
||||||
@@ -88,16 +88,26 @@ interface SectionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Section = ({ section, onReferenceClick }: SectionProps) => {
|
const Section = ({ section, onReferenceClick }: SectionProps) => {
|
||||||
const HeadingTag = `h${Math.min(section.level + 1, 6)}` as keyof JSX.IntrinsicElements
|
const level = Math.min(section.level + 1, 6)
|
||||||
|
const headingClass = clsx(
|
||||||
|
'font-semibold text-white mb-3',
|
||||||
|
section.level === 1 ? 'text-xl' : 'text-lg'
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderHeading = () => {
|
||||||
|
switch (level) {
|
||||||
|
case 2: return <h2 className={headingClass}>{section.title}</h2>
|
||||||
|
case 3: return <h3 className={headingClass}>{section.title}</h3>
|
||||||
|
case 4: return <h4 className={headingClass}>{section.title}</h4>
|
||||||
|
case 5: return <h5 className={headingClass}>{section.title}</h5>
|
||||||
|
case 6: return <h6 className={headingClass}>{section.title}</h6>
|
||||||
|
default: return <h2 className={headingClass}>{section.title}</h2>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mb-6">
|
<section className="mb-6">
|
||||||
<HeadingTag className={clsx(
|
{renderHeading()}
|
||||||
'font-semibold text-white mb-3',
|
|
||||||
section.level === 1 ? 'text-xl' : 'text-lg'
|
|
||||||
)}>
|
|
||||||
{section.title}
|
|
||||||
</HeadingTag>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{section.content.map((item, idx) => {
|
{section.content.map((item, idx) => {
|
||||||
|
|||||||
13
src/components/blueprint/BlueprintPage.tsx
Normal file
13
src/components/blueprint/BlueprintPage.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import Scene3D from './Scene3D';
|
||||||
|
import DetailPanel from './DetailPanel';
|
||||||
|
|
||||||
|
export default function BlueprintPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Scene3D />
|
||||||
|
</div>
|
||||||
|
<DetailPanel />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
118
src/components/blueprint/DetailPanel.tsx
Normal file
118
src/components/blueprint/DetailPanel.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { useBlueprintStore } from '../../store/blueprintStore';
|
||||||
|
|
||||||
|
export default function DetailPanel() {
|
||||||
|
const { blueprint, selectedNode } = useBlueprintStore();
|
||||||
|
|
||||||
|
if (!selectedNode) {
|
||||||
|
return (
|
||||||
|
<div className="w-72 border-l border-zinc-800 p-4">
|
||||||
|
<div className="text-xs text-zinc-600">Select a node</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRoot = selectedNode === 'root';
|
||||||
|
const subsystem = blueprint.subsystems.find(s => s.id === selectedNode);
|
||||||
|
const module = blueprint.modules.find(m => m.id === selectedNode);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-72 border-l border-zinc-800 p-4 overflow-y-auto text-sm">
|
||||||
|
{isRoot && (
|
||||||
|
<>
|
||||||
|
<h2 className="text-base font-medium text-white mb-1">{blueprint.meta.name}</h2>
|
||||||
|
<div className="text-xs text-zinc-500 mb-4">{blueprint.meta.description}</div>
|
||||||
|
<div className="space-y-2 text-xs">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-zinc-600">Version</span>
|
||||||
|
<span className="text-zinc-400">{blueprint.meta.version}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-zinc-600">Type</span>
|
||||||
|
<span className="text-zinc-400">{blueprint.meta.type}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-zinc-600">Runtime</span>
|
||||||
|
<span className="text-zinc-400">{blueprint.meta.target_runtime}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-zinc-600">Subsystems</span>
|
||||||
|
<span className="text-zinc-400">{blueprint.subsystems.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-zinc-600">Modules</span>
|
||||||
|
<span className="text-zinc-400">{blueprint.modules.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subsystem && (
|
||||||
|
<>
|
||||||
|
<h2 className="text-base font-medium text-white mb-4">{subsystem.name}</h2>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="text-[10px] font-medium text-zinc-500 uppercase tracking-wider mb-2">Responsibilities</div>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{subsystem.responsibilities.map((r, i) => (
|
||||||
|
<li key={i} className="text-xs text-zinc-400">· {r}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="text-[10px] font-medium text-zinc-500 uppercase tracking-wider mb-2">Provides</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{subsystem.provides.map((p, i) => (
|
||||||
|
<span key={i} className="px-1.5 py-0.5 bg-zinc-800 text-zinc-400 text-[10px]">{p}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{subsystem.depends_on.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] font-medium text-zinc-500 uppercase tracking-wider mb-2">Depends On</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{subsystem.depends_on.map((d, i) => (
|
||||||
|
<span key={i} className="px-1.5 py-0.5 bg-zinc-800 text-zinc-400 text-[10px]">{d}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{module && (
|
||||||
|
<>
|
||||||
|
<h2 className="text-base font-medium text-white mb-4">{module.name}</h2>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="text-[10px] font-medium text-zinc-500 uppercase tracking-wider mb-1">Parent</div>
|
||||||
|
<span className="text-xs text-zinc-400">{module.parent_subsystem}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="text-[10px] font-medium text-zinc-500 uppercase tracking-wider mb-1">Responsibility</div>
|
||||||
|
<p className="text-xs text-zinc-400">{module.responsibility}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] font-medium text-zinc-500 uppercase tracking-wider mb-2">Public API</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{module.public_api.map((api, i) => (
|
||||||
|
<div key={i} className="bg-zinc-900 p-2">
|
||||||
|
<div className="text-xs text-zinc-300 font-mono">{api.fn}()</div>
|
||||||
|
<div className="text-[10px] text-zinc-600 mt-1">
|
||||||
|
params: {api.params.map(p => p.name).join(', ') || 'none'}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-zinc-600">
|
||||||
|
returns: {api.returns.type}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/components/blueprint/Scene3D.tsx
Normal file
23
src/components/blueprint/Scene3D.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Canvas } from '@react-three/fiber';
|
||||||
|
import { OrbitControls, PerspectiveCamera } from '@react-three/drei';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
import SystemStructure from './SystemStructure';
|
||||||
|
|
||||||
|
export default function Scene3D() {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full bg-black">
|
||||||
|
<Canvas>
|
||||||
|
<PerspectiveCamera makeDefault position={[12, 10, 12]} fov={50} />
|
||||||
|
<OrbitControls enablePan enableZoom enableRotate />
|
||||||
|
|
||||||
|
<ambientLight intensity={0.8} />
|
||||||
|
<directionalLight position={[10, 20, 10]} intensity={1.5} />
|
||||||
|
<pointLight position={[-10, 10, -10]} intensity={0.5} color="#ffffff" />
|
||||||
|
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<SystemStructure />
|
||||||
|
</Suspense>
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
311
src/components/blueprint/SystemStructure.tsx
Normal file
311
src/components/blueprint/SystemStructure.tsx
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
180
src/data/sampleData.ts
Normal file
180
src/data/sampleData.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
export interface Subsystem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
responsibilities: string[];
|
||||||
|
provides: string[];
|
||||||
|
depends_on: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Module {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
parent_subsystem: string;
|
||||||
|
responsibility: string;
|
||||||
|
public_api: {
|
||||||
|
fn: string;
|
||||||
|
params: { name: string; type: string }[];
|
||||||
|
returns: { type: string };
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemMeta {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
target_runtime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlueprintData {
|
||||||
|
meta: SystemMeta;
|
||||||
|
subsystems: Subsystem[];
|
||||||
|
modules: Module[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sampleBlueprint: BlueprintData = {
|
||||||
|
meta: {
|
||||||
|
name: 'UnityEngine',
|
||||||
|
version: '0.1.0',
|
||||||
|
type: 'game-engine',
|
||||||
|
description: '轻量级3D游戏引擎,支持场景管理、渲染、物理、脚本系统',
|
||||||
|
target_runtime: 'C++17 / C#'
|
||||||
|
},
|
||||||
|
subsystems: [
|
||||||
|
{
|
||||||
|
id: 'Core',
|
||||||
|
name: 'Core',
|
||||||
|
responsibilities: ['基础数据类型和算法', '内存管理', '平台抽象层'],
|
||||||
|
provides: ['IAllocator', 'IPlatform', 'IFileSystem'],
|
||||||
|
depends_on: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Rendering',
|
||||||
|
name: 'Rendering',
|
||||||
|
responsibilities: ['渲染管线管理', '渲染资源管理', 'Shader管理', 'Camera管理'],
|
||||||
|
provides: ['IRenderPipeline', 'IRenderResource', 'IShader', 'ICamera'],
|
||||||
|
depends_on: ['Core']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Physics',
|
||||||
|
name: 'Physics',
|
||||||
|
responsibilities: ['物理模拟', '碰撞检测', '刚体/关节系统'],
|
||||||
|
provides: ['IPhysicsWorld', 'ICollider', 'IRigidbody'],
|
||||||
|
depends_on: ['Core']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Scripting',
|
||||||
|
name: 'Scripting',
|
||||||
|
responsibilities: ['脚本生命周期管理', 'C#运行时集成', '组件系统'],
|
||||||
|
provides: ['IScriptRuntime', 'IMonoBehaviour', 'IComponent'],
|
||||||
|
depends_on: ['Core']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Scene',
|
||||||
|
name: 'Scene',
|
||||||
|
responsibilities: ['场景图管理', 'GameObject层级管理', '变换层级'],
|
||||||
|
provides: ['IScene', 'IGameObject', 'ITransform'],
|
||||||
|
depends_on: ['Core']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Asset',
|
||||||
|
name: 'Asset',
|
||||||
|
responsibilities: ['资源加载/卸载', '资源引用计数', '资源格式支持'],
|
||||||
|
provides: ['IAssetLoader', 'IAssetDatabase'],
|
||||||
|
depends_on: ['Core']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Input',
|
||||||
|
name: 'Input',
|
||||||
|
responsibilities: ['输入事件采集', '输入映射'],
|
||||||
|
provides: ['IInputSystem'],
|
||||||
|
depends_on: ['Core']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Platform',
|
||||||
|
name: 'Platform',
|
||||||
|
responsibilities: ['平台特定实现', '窗口管理', '主循环'],
|
||||||
|
provides: ['IWindow', 'IApplication'],
|
||||||
|
depends_on: ['Core']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
id: 'RHI',
|
||||||
|
name: 'RHI',
|
||||||
|
parent_subsystem: 'Rendering',
|
||||||
|
responsibility: '渲染硬件抽象层',
|
||||||
|
public_api: [
|
||||||
|
{ fn: 'CreateGraphicsPipeline', params: [{ name: 'desc', type: 'GraphicsPipelineDesc' }], returns: { type: 'IPipeline' } },
|
||||||
|
{ fn: 'Draw', params: [{ name: 'pipeline', type: 'IPipeline' }, { name: 'mesh', type: 'IMesh' }], returns: { type: 'void' } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'RenderPipeline',
|
||||||
|
name: 'RenderPipeline',
|
||||||
|
parent_subsystem: 'Rendering',
|
||||||
|
responsibility: '渲染管线调度',
|
||||||
|
public_api: [
|
||||||
|
{ fn: 'Render', params: [{ name: 'scene', type: 'IScene' }, { name: 'camera', type: 'ICamera' }], returns: { type: 'void' } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ShaderManager',
|
||||||
|
name: 'ShaderManager',
|
||||||
|
parent_subsystem: 'Rendering',
|
||||||
|
responsibility: 'Shader编译和缓存',
|
||||||
|
public_api: [
|
||||||
|
{ fn: 'LoadShader', params: [{ name: 'path', type: 'string' }], returns: { type: 'IShader' } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PhysicsWorld',
|
||||||
|
name: 'PhysicsWorld',
|
||||||
|
parent_subsystem: 'Physics',
|
||||||
|
responsibility: '物理世界模拟',
|
||||||
|
public_api: [
|
||||||
|
{ fn: 'Step', params: [{ name: 'dt', type: 'float' }], returns: { type: 'void' } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'MonoBehaviour',
|
||||||
|
name: 'MonoBehaviour',
|
||||||
|
parent_subsystem: 'Scripting',
|
||||||
|
responsibility: '脚本组件基类',
|
||||||
|
public_api: [
|
||||||
|
{ fn: 'Awake', params: [], returns: { type: 'void' } },
|
||||||
|
{ fn: 'Start', params: [], returns: { type: 'void' } },
|
||||||
|
{ fn: 'Update', params: [{ name: 'dt', type: 'float' }], returns: { type: 'void' } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Transform',
|
||||||
|
name: 'Transform',
|
||||||
|
parent_subsystem: 'Scene',
|
||||||
|
responsibility: '变换层级管理',
|
||||||
|
public_api: [
|
||||||
|
{ fn: 'SetParent', params: [{ name: 'parent', type: 'ITransform' }], returns: { type: 'void' } },
|
||||||
|
{ fn: 'LocalToWorld', params: [{ name: 'localPos', type: 'vec3' }], returns: { type: 'vec3' } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'GameObject',
|
||||||
|
name: 'GameObject',
|
||||||
|
parent_subsystem: 'Scene',
|
||||||
|
responsibility: '场景对象管理',
|
||||||
|
public_api: [
|
||||||
|
{ fn: 'AddComponent', params: [{ name: 'type', type: 'type_info' }], returns: { type: 'IComponent' } },
|
||||||
|
{ fn: 'GetComponent', params: [{ name: 'type', type: 'type_info' }], returns: { type: 'IComponent' } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'AssetLoader',
|
||||||
|
name: 'AssetLoader',
|
||||||
|
parent_subsystem: 'Asset',
|
||||||
|
responsibility: '资源异步加载',
|
||||||
|
public_api: [
|
||||||
|
{ fn: 'LoadAsync', params: [{ name: 'path', type: 'string' }], returns: { type: 'AssetFuture' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--sidebar-opacity: 0.8;
|
--sidebar-opacity: 0.8;
|
||||||
|
|||||||
15
src/store/blueprintStore.ts
Normal file
15
src/store/blueprintStore.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import type { BlueprintData } from '../data/sampleData';
|
||||||
|
import { sampleBlueprint } from '../data/sampleData';
|
||||||
|
|
||||||
|
interface BlueprintStore {
|
||||||
|
blueprint: BlueprintData;
|
||||||
|
selectedNode: string | null;
|
||||||
|
setSelectedNode: (id: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBlueprintStore = create<BlueprintStore>((set) => ({
|
||||||
|
blueprint: sampleBlueprint,
|
||||||
|
selectedNode: null,
|
||||||
|
setSelectedNode: (id) => set({ selectedNode: id }),
|
||||||
|
}));
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src", "src/**/*.tsx", "src/**/*.ts"],
|
||||||
|
"exclude": ["src/docs"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
|||||||
1
tsconfig.node.tsbuildinfo
Normal file
1
tsconfig.node.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/apidocviewer.tsx","./src/components/doccontent.tsx","./src/components/doctree.tsx","./src/components/blueprint/blueprintpage.tsx","./src/components/blueprint/detailpanel.tsx","./src/components/blueprint/scene3d.tsx","./src/components/blueprint/systemstructure.tsx","./src/data/sampledata.ts","./src/lib/parser.ts","./src/lib/types.ts","./src/store/blueprintstore.ts"],"version":"5.9.3"}
|
||||||
2
vite.config.d.ts
vendored
Normal file
2
vite.config.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
declare const _default: import("vite").UserConfig;
|
||||||
|
export default _default;
|
||||||
16
vite.config.js
Normal file
16
vite.config.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3001,
|
||||||
|
strictPort: false,
|
||||||
|
},
|
||||||
|
assetsInclude: ['**/*.md'],
|
||||||
|
});
|
||||||
@@ -13,4 +13,5 @@ export default defineConfig({
|
|||||||
port: 3001,
|
port: 3001,
|
||||||
strictPort: false,
|
strictPort: false,
|
||||||
},
|
},
|
||||||
|
assetsInclude: ['**/*.md'],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user