feat: merge XCBluePrint 3D visualization into project
This commit is contained in:
4458
package-lock.json
generated
4458
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",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.511.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"three": "^0.183.2",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@tailwindcss/postcss": "^4.2.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",
|
||||
"playwright": "^1.58.2",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^6.3.5"
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"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 {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
51
src/App.tsx
51
src/App.tsx
@@ -1,9 +1,50 @@
|
||||
import { ApiDocViewer } from './components/ApiDocViewer'
|
||||
import { config } from './config'
|
||||
import { useState } from 'react';
|
||||
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() {
|
||||
document.title = config.projectName
|
||||
return <ApiDocViewer />
|
||||
const [currentPage, setCurrentPage] = useState<Page>('docs');
|
||||
|
||||
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'
|
||||
|
||||
interface DocContentProps {
|
||||
@@ -88,16 +88,26 @@ interface 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 (
|
||||
<section className="mb-6">
|
||||
<HeadingTag className={clsx(
|
||||
'font-semibold text-white mb-3',
|
||||
section.level === 1 ? 'text-xl' : 'text-lg'
|
||||
)}>
|
||||
{section.title}
|
||||
</HeadingTag>
|
||||
{renderHeading()}
|
||||
|
||||
<div className="space-y-3">
|
||||
{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;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--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/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src", "src/**/*.tsx", "src/**/*.ts"],
|
||||
"exclude": ["src/docs"],
|
||||
"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,
|
||||
strictPort: false,
|
||||
},
|
||||
assetsInclude: ['**/*.md'],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user