Spaces:
Build error
Build error
| import type { NN3DModel, Position3D } from '@/schema/types'; | |
| /** | |
| * Layout algorithm type | |
| */ | |
| export type LayoutType = 'layered' | 'force' | 'circular' | 'hierarchical' | 'custom'; | |
| /** | |
| * Layout configuration options | |
| */ | |
| export interface LayoutOptions { | |
| type: LayoutType; | |
| layerSpacing: number; | |
| nodeSpacing: number; | |
| direction: 'horizontal' | 'vertical' | 'depth'; | |
| centerGraph: boolean; | |
| } | |
| /** | |
| * Default layout options | |
| */ | |
| const DEFAULT_OPTIONS: LayoutOptions = { | |
| type: 'layered', | |
| layerSpacing: 3.0, | |
| nodeSpacing: 2.0, | |
| direction: 'horizontal', | |
| centerGraph: true, | |
| }; | |
| /** | |
| * Layout result | |
| */ | |
| export interface LayoutResult { | |
| positions: Map<string, Position3D>; | |
| bounds: { | |
| min: Position3D; | |
| max: Position3D; | |
| center: Position3D; | |
| }; | |
| } | |
| /** | |
| * Compute layout for a neural network graph | |
| */ | |
| export function computeLayout( | |
| model: NN3DModel, | |
| options: Partial<LayoutOptions> = {} | |
| ): LayoutResult { | |
| const opts = { ...DEFAULT_OPTIONS, ...options }; | |
| switch (opts.type) { | |
| case 'layered': | |
| return computeLayeredLayout(model, opts); | |
| case 'force': | |
| return computeForceLayout(model, opts); | |
| case 'circular': | |
| return computeCircularLayout(model, opts); | |
| case 'hierarchical': | |
| return computeHierarchicalLayout(model, opts); | |
| default: | |
| return computeLayeredLayout(model, opts); | |
| } | |
| } | |
| /** | |
| * Layered layout - nodes arranged in layers based on depth | |
| */ | |
| function computeLayeredLayout(model: NN3DModel, options: LayoutOptions): LayoutResult { | |
| const { nodes, edges } = model.graph; | |
| const positions = new Map<string, Position3D>(); | |
| // Build adjacency and compute depths | |
| const inDegree = new Map<string, number>(); | |
| const adjacency = new Map<string, string[]>(); | |
| nodes.forEach(node => { | |
| inDegree.set(node.id, 0); | |
| adjacency.set(node.id, []); | |
| }); | |
| edges.forEach(edge => { | |
| inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1); | |
| adjacency.get(edge.source)?.push(edge.target); | |
| }); | |
| // Topological sort to determine layers | |
| const layers: string[][] = []; | |
| const nodeDepths = new Map<string, number>(); | |
| const queue: string[] = []; | |
| // Start with nodes having no incoming edges | |
| nodes.forEach(node => { | |
| if (inDegree.get(node.id) === 0) { | |
| queue.push(node.id); | |
| nodeDepths.set(node.id, 0); | |
| } | |
| }); | |
| // BFS to assign depths | |
| while (queue.length > 0) { | |
| const nodeId = queue.shift()!; | |
| const depth = nodeDepths.get(nodeId) || 0; | |
| // Ensure layer array exists | |
| if (!layers[depth]) { | |
| layers[depth] = []; | |
| } | |
| layers[depth].push(nodeId); | |
| // Process neighbors | |
| const neighbors = adjacency.get(nodeId) || []; | |
| for (const neighbor of neighbors) { | |
| const newDegree = (inDegree.get(neighbor) || 1) - 1; | |
| inDegree.set(neighbor, newDegree); | |
| if (newDegree === 0) { | |
| nodeDepths.set(neighbor, depth + 1); | |
| queue.push(neighbor); | |
| } | |
| } | |
| } | |
| // Handle cycles (nodes not yet placed) | |
| nodes.forEach(node => { | |
| if (!nodeDepths.has(node.id)) { | |
| const lastLayerIndex = layers.length; | |
| if (!layers[lastLayerIndex]) { | |
| layers[lastLayerIndex] = []; | |
| } | |
| layers[lastLayerIndex].push(node.id); | |
| nodeDepths.set(node.id, lastLayerIndex); | |
| } | |
| }); | |
| // Compute positions | |
| let maxWidth = 0; | |
| layers.forEach(layer => { | |
| maxWidth = Math.max(maxWidth, layer.length); | |
| }); | |
| layers.forEach((layer, layerIndex) => { | |
| const layerWidth = (layer.length - 1) * options.nodeSpacing; | |
| const startOffset = -layerWidth / 2; | |
| layer.forEach((nodeId, nodeIndex) => { | |
| let x: number, y: number, z: number; | |
| if (options.direction === 'vertical') { | |
| // Vertical: layers stack along Y (top to bottom), nodes spread on X | |
| x = startOffset + nodeIndex * options.nodeSpacing; | |
| y = -layerIndex * options.layerSpacing; | |
| z = 0; | |
| } else if (options.direction === 'horizontal') { | |
| // Horizontal: layers stack along X (left to right), nodes spread on Y | |
| x = layerIndex * options.layerSpacing; | |
| y = startOffset + nodeIndex * options.nodeSpacing; | |
| z = 0; | |
| } else { | |
| // Depth: layers stack along Z (front to back), nodes spread on X | |
| x = startOffset + nodeIndex * options.nodeSpacing; | |
| y = 0; | |
| z = -layerIndex * options.layerSpacing; | |
| } | |
| positions.set(nodeId, { x, y, z }); | |
| }); | |
| }); | |
| return { | |
| positions, | |
| bounds: computeBounds(positions), | |
| }; | |
| } | |
| /** | |
| * Force-directed layout using simple spring simulation | |
| */ | |
| function computeForceLayout(model: NN3DModel, options: LayoutOptions): LayoutResult { | |
| const { nodes, edges } = model.graph; | |
| const positions = new Map<string, Position3D>(); | |
| const velocities = new Map<string, Position3D>(); | |
| // Initialize random positions | |
| nodes.forEach((node, i) => { | |
| const angle = (i / nodes.length) * Math.PI * 2; | |
| const radius = 5 + Math.random() * 5; | |
| positions.set(node.id, { | |
| x: Math.cos(angle) * radius, | |
| y: Math.sin(angle) * radius, | |
| z: (Math.random() - 0.5) * 5, | |
| }); | |
| velocities.set(node.id, { x: 0, y: 0, z: 0 }); | |
| }); | |
| // Simulation parameters | |
| const iterations = 100; | |
| const repulsion = 50; | |
| const attraction = 0.1; | |
| const damping = 0.9; | |
| for (let iter = 0; iter < iterations; iter++) { | |
| // Repulsion between all nodes | |
| for (let i = 0; i < nodes.length; i++) { | |
| for (let j = i + 1; j < nodes.length; j++) { | |
| const posA = positions.get(nodes[i].id)!; | |
| const posB = positions.get(nodes[j].id)!; | |
| const dx = posB.x - posA.x; | |
| const dy = posB.y - posA.y; | |
| const dz = posB.z - posA.z; | |
| const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) + 0.1; | |
| const force = repulsion / (dist * dist); | |
| const fx = (dx / dist) * force; | |
| const fy = (dy / dist) * force; | |
| const fz = (dz / dist) * force; | |
| const velA = velocities.get(nodes[i].id)!; | |
| const velB = velocities.get(nodes[j].id)!; | |
| velA.x -= fx; | |
| velA.y -= fy; | |
| velA.z -= fz; | |
| velB.x += fx; | |
| velB.y += fy; | |
| velB.z += fz; | |
| } | |
| } | |
| // Attraction along edges | |
| edges.forEach(edge => { | |
| const posA = positions.get(edge.source); | |
| const posB = positions.get(edge.target); | |
| if (!posA || !posB) return; | |
| const dx = posB.x - posA.x; | |
| const dy = posB.y - posA.y; | |
| const dz = posB.z - posA.z; | |
| const dist = Math.sqrt(dx * dx + dy * dy + dz * dz); | |
| const force = attraction * (dist - options.nodeSpacing); | |
| const fx = (dx / dist) * force; | |
| const fy = (dy / dist) * force; | |
| const fz = (dz / dist) * force; | |
| const velA = velocities.get(edge.source)!; | |
| const velB = velocities.get(edge.target)!; | |
| velA.x += fx; | |
| velA.y += fy; | |
| velA.z += fz; | |
| velB.x -= fx; | |
| velB.y -= fy; | |
| velB.z -= fz; | |
| }); | |
| // Apply velocities with damping | |
| nodes.forEach(node => { | |
| const pos = positions.get(node.id)!; | |
| const vel = velocities.get(node.id)!; | |
| pos.x += vel.x; | |
| pos.y += vel.y; | |
| pos.z += vel.z; | |
| vel.x *= damping; | |
| vel.y *= damping; | |
| vel.z *= damping; | |
| }); | |
| } | |
| // Center the graph | |
| if (options.centerGraph) { | |
| const bounds = computeBounds(positions); | |
| positions.forEach((pos, id) => { | |
| positions.set(id, { | |
| x: pos.x - bounds.center.x, | |
| y: pos.y - bounds.center.y, | |
| z: pos.z - bounds.center.z, | |
| }); | |
| }); | |
| } | |
| return { | |
| positions, | |
| bounds: computeBounds(positions), | |
| }; | |
| } | |
| /** | |
| * Circular layout - nodes arranged in a circle by layer | |
| */ | |
| function computeCircularLayout(model: NN3DModel, options: LayoutOptions): LayoutResult { | |
| const { nodes } = model.graph; | |
| const positions = new Map<string, Position3D>(); | |
| const nodeCount = nodes.length; | |
| const radius = Math.max(nodeCount * options.nodeSpacing / (2 * Math.PI), 5); | |
| nodes.forEach((node, i) => { | |
| const angle = (i / nodeCount) * Math.PI * 2; | |
| positions.set(node.id, { | |
| x: Math.cos(angle) * radius, | |
| y: Math.sin(angle) * radius, | |
| z: 0, | |
| }); | |
| }); | |
| return { | |
| positions, | |
| bounds: computeBounds(positions), | |
| }; | |
| } | |
| /** | |
| * Hierarchical layout - tree-like structure | |
| */ | |
| function computeHierarchicalLayout(model: NN3DModel, options: LayoutOptions): LayoutResult { | |
| // Use layered layout as base, but with different spacing | |
| return computeLayeredLayout(model, { | |
| ...options, | |
| layerSpacing: options.layerSpacing * 1.5, | |
| }); | |
| } | |
| /** | |
| * Compute bounding box of positions | |
| */ | |
| function computeBounds(positions: Map<string, Position3D>): { | |
| min: Position3D; | |
| max: Position3D; | |
| center: Position3D; | |
| } { | |
| let minX = Infinity, minY = Infinity, minZ = Infinity; | |
| let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; | |
| positions.forEach(pos => { | |
| minX = Math.min(minX, pos.x); | |
| minY = Math.min(minY, pos.y); | |
| minZ = Math.min(minZ, pos.z); | |
| maxX = Math.max(maxX, pos.x); | |
| maxY = Math.max(maxY, pos.y); | |
| maxZ = Math.max(maxZ, pos.z); | |
| }); | |
| return { | |
| min: { x: minX, y: minY, z: minZ }, | |
| max: { x: maxX, y: maxY, z: maxZ }, | |
| center: { | |
| x: (minX + maxX) / 2, | |
| y: (minY + maxY) / 2, | |
| z: (minZ + maxZ) / 2, | |
| }, | |
| }; | |
| } | |
| export { computeBounds }; | |