Skip to main content
Glama
interactive_signal_map.ts7.64 kB
/** * Interactive Signal Map Renderer * * D3.js force-directed graph with interactive controls. * Replaces PlaceholderSignalMapRenderer for production use. * * @module artifacts/renderers/interactive_signal_map */ import { ArtifactRenderer, type ArtifactMetadata } from '../types.js'; import { SignalDefinition } from '../parsers/gdscript_parser.js'; import { D3GraphRenderer, type GraphNode, type GraphEdge } from '../visualizations/D3GraphRenderer.js'; import { ClusterVisualizer, type Cluster } from '../visualizations/ClusterVisualizer.js'; import { SignalRelationshipParser } from '../../parsers/signal_relationship_parser.js'; /** * Signal map input data */ export interface SignalMapData { signals: SignalDefinition[]; projectPath: string; metadata?: { eventBusCount: number; signalBusCount: number; }; clusters?: Cluster[]; // Optional cluster data from hierarchical clustering } /** * Interactive signal map renderer using D3.js * * Features: * - Force-directed graph layout * - Zoom and pan controls * - Drag-to-reposition nodes * - Cluster visualization (if cluster data provided) * - Real-time filtering (Phase 2 integration) * - Signal relationship detection (emit/connect tracking) * * @example Basic usage * ```typescript * import { InteractiveSignalMapRenderer } from './renderers/interactive_signal_map.js'; * * const renderer = new InteractiveSignalMapRenderer(); * * const signalMapData = { * signals: [ * { name: 'player_health_changed', source: 'EventBus', params: ['new_health', 'old_health'], filePath: 'autoload/EventBus.gd', line: 5 }, * { name: 'enemy_spawned', source: 'EventBus', params: ['enemy_type', 'position'], filePath: 'autoload/EventBus.gd', line: 6 } * ], * projectPath: '/path/to/godot/project', * metadata: { * eventBusCount: 2, * signalBusCount: 0 * } * }; * * const html = await renderer.render(signalMapData, { title: 'Combat Signals', description: 'Core gameplay signals' }); * // Returns: HTML with D3 force-directed graph, automatic relationship detection * ``` * * @example With clustering * ```typescript * const signalMapDataWithClusters = { * signals: [...], // 100+ signals * projectPath: '/path/to/project', * clusters: [ * { id: 'combat', label: 'Combat Signals', signalIds: ['player_attack', 'enemy_hit', 'damage_dealt'] }, * { id: 'ui', label: 'UI Signals', signalIds: ['health_bar_updated', 'menu_opened'] } * ] * }; * * const html = await renderer.render(signalMapDataWithClusters); * // Renders graph with cluster boundaries (convex hulls) and labels * ``` */ export class InteractiveSignalMapRenderer implements ArtifactRenderer { readonly type = 'signal_map_interactive'; private readonly d3Renderer: D3GraphRenderer; private readonly relationshipParser: SignalRelationshipParser; constructor() { this.d3Renderer = new D3GraphRenderer(1200, 800); this.relationshipParser = new SignalRelationshipParser(); } async render(data: unknown, metadata?: ArtifactMetadata): Promise<string> { const mapData = data as SignalMapData; const signals = mapData.signals || []; // Convert signals to graph nodes const nodes: GraphNode[] = signals.map((sig, index) => ({ id: sig.name, label: sig.name, type: sig.source, // "EventBus" or "SignalBus" filePath: sig.filePath, line: sig.line, params: sig.params, index })); // Phase 3: Extract real connections from GDScript analysis const edges: GraphEdge[] = await this.generateRealEdges(mapData.projectPath, nodes); // Generate base D3 graph let html = this.d3Renderer.generateForceDirectedGraph(nodes, edges); // Add cluster boundaries if cluster data provided if (mapData.clusters && mapData.clusters.length > 0) { html = this.injectClusterVisualization(html, mapData.clusters, nodes); } // Add metadata header html = this.addMetadataHeader(html, mapData, metadata); return html; } /** * Generate real signal connection edges from GDScript analysis * Replaces Phase 3 placeholder sample edges with actual emit/connect relationships */ private async generateRealEdges(projectPath: string, nodes: GraphNode[]): Promise<GraphEdge[]> { try { // Parse signal connections using relationship parser const connections = await this.relationshipParser.parseConnections(projectPath); // Convert SignalConnection[] to GraphEdge[] format const edges: GraphEdge[] = connections.map(conn => ({ source: conn.emitter, target: conn.listener, type: conn.type as 'emit' | 'connect' })); return edges; } catch (error) { console.warn('Failed to parse signal connections, falling back to sample edges:', error); // Fallback to sample edges if parser fails return this.generateSampleEdges(nodes); } } /** * Generate sample edges for testing (fallback only) * Creates sequential connections between nodes for visualization testing */ private generateSampleEdges(nodes: GraphNode[]): GraphEdge[] { const edges: GraphEdge[] = []; // Create a few sample connections for (let i = 0; i < Math.min(nodes.length - 1, 10); i++) { edges.push({ source: nodes[i].id, target: nodes[i + 1].id, type: 'emit' }); } return edges; } /** * Inject cluster visualization into D3 graph HTML */ private injectClusterVisualization( html: string, clusters: Cluster[], nodes: GraphNode[] ): string { const visualizer = new ClusterVisualizer(clusters); // Generate cluster boundary SVG const clusterBoundaries = visualizer.generateClusterBoundaries(clusters); // Generate color mapping for nodes const nodeColorMap = visualizer.colorCodeNodes(nodes, clusters); // Inject cluster boundaries before nodes html = html.replace( '// Render nodes', `// Render cluster boundaries g.append('g') .attr('class', 'cluster-boundaries') .html(\`${clusterBoundaries}\`); // Render nodes` ); // Update node colors based on cluster let colorUpdateScript = '\n // Update node colors based on clusters\n'; nodeColorMap.forEach((color, nodeId) => { colorUpdateScript += ` node.filter(d => d.id === '${nodeId}').attr('fill', '${color}');\n`; }); html = html.replace( 'window.restartSimulation', colorUpdateScript + '\n window.restartSimulation' ); return html; } /** * Add metadata header to visualization */ private addMetadataHeader( html: string, mapData: SignalMapData, metadata?: ArtifactMetadata ): string { const title = metadata?.title || 'Signal Map'; const description = metadata?.description || `${mapData.signals.length} signals`; const header = ` <div class="metadata-header" style="position: absolute; top: 100px; left: 20px; background: rgba(30, 30, 30, 0.9); border: 1px solid #444; border-radius: 8px; padding: 15px; z-index: 1000; max-width: 300px;"> <h2 style="margin: 0 0 10px 0; color: #007acc; font-size: 16px;">${title}</h2> <p style="margin: 0; font-size: 12px;">${description}</p> ${mapData.metadata ? ` <div style="margin-top: 10px; font-size: 11px; color: #999;"> <div>EventBus: ${mapData.metadata.eventBusCount}</div> <div>SignalBus: ${mapData.metadata.signalBusCount}</div> </div> ` : ''} </div> `; return html.replace('<div class="controls">', header + '\n <div class="controls">'); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/EricA1019/CTS_MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server