15 KiB
15 KiB
Studio UI Wiring Guide
Complete mapping: Godot Editor Features → StudioBridge API → Studio Components
🎯 Goal
Wire Studio UI (TypeScript/React) to have 100% feature parity with Godot editor using the StudioBridge API.
📦 Part 1: What Godot Has (C++)
Core Editor Panels
editor/
├── docks/
│ ├── scene_tree_dock.cpp → Scene hierarchy
│ ├── inspector_dock.cpp → Property editor
│ ├── filesystem_dock.cpp → File browser
│ └── node_dock.cpp → Node info
├── plugins/
│ ├── script_editor_plugin.cpp → Code editor
│ ├── canvas_item_editor_plugin.cpp → 2D viewport
│ └── node_3d_editor_plugin.cpp → 3D viewport
└── editor_node.cpp → Main window/orchestration
🔌 Part 2: StudioBridge API (What We Already Built)
✅ Already Implemented (Basic CRUD)
// Scene management
loadScene(path) → Load .tscn file
saveScene(path) → Save current scene
getSceneTree() → Get full hierarchy
// Node operations
createNode(type, parent, name) → Add new node
deleteNode(path) → Remove node
selectNode(path) → Set active node
// Properties
setProperty(path, property, value) → Update node property
getProperty(path, property) → Read property value
// Game control
runGame() → Launch game instance
stopGame() → Stop game
⏳ Need to Add (Advanced Features)
Scene Tree Operations
// Add to studio_bridge.cpp:
Dictionary move_node(node_path, new_parent_path, position)
Dictionary duplicate_node(node_path)
Dictionary rename_node(node_path, new_name)
Dictionary reparent_node(node_path, new_parent_path)
Dictionary copy_node(node_path) // to clipboard
Dictionary paste_node(parent_path) // from clipboard
Dictionary get_node_groups(node_path)
Dictionary add_to_group(node_path, group_name)
Inspector/Properties
Dictionary get_all_properties(node_path) // Get full property list
Dictionary get_property_info(node_path, property) // Type, hint, range
Dictionary reset_property(node_path, property) // Reset to default
Array get_inspectable_nodes() // Multi-select support
Resource/Asset Management
Dictionary list_directory(path) // File system browsing
Dictionary import_asset(path, type)
Dictionary create_resource(type, path)
Dictionary load_resource(path)
Array get_recent_files()
Script Editor
Dictionary attach_script(node_path, script_path)
Dictionary detach_script(node_path)
String get_node_script(node_path)
Dictionary save_script(path, content)
Dictionary open_script(path)
3D/2D Viewport
Dictionary get_viewport_texture() // Stream 3D view to Studio
Dictionary set_camera_position(x, y, z)
Dictionary set_camera_rotation(x, y, z)
Dictionary toggle_gizmos(enabled)
Dictionary set_grid_visible(visible)
🎨 Part 3: Studio UI Components (TypeScript/React)
File Structure
studio/src/
├── engine/
│ ├── bridge.ts ← API client (wraps fetch calls)
│ ├── types.ts ← Node, Property, Scene types
│ └── hooks.ts ← React hooks (useSceneTree, etc)
├── components/
│ ├── SceneTree/
│ │ ├── SceneTreePanel.tsx ← Left sidebar hierarchy
│ │ ├── NodeItem.tsx ← Tree node component
│ │ └── ContextMenu.tsx ← Right-click menu
│ ├── Inspector/
│ │ ├── InspectorPanel.tsx ← Right sidebar properties
│ │ ├── PropertyEditor.tsx ← Individual property input
│ │ └── ResourcePicker.tsx ← Resource selection
│ ├── Viewport/
│ │ ├── Viewport2D.tsx ← 2D scene view
│ │ ├── Viewport3D.tsx ← 3D scene view (WebGL/Three.js)
│ │ └── ViewportControls.tsx ← Camera controls
│ ├── FileSystem/
│ │ ├── FileSystemPanel.tsx ← Bottom file browser
│ │ └── AssetPreview.tsx ← Preview images/models
│ ├── ScriptEditor/
│ │ ├── CodeEditor.tsx ← Monaco editor wrapper
│ │ └── ScriptTabs.tsx ← Open script tabs
│ └── Toolbar/
│ ├── MainToolbar.tsx ← Top toolbar (Play, Stop, etc)
│ └── NodeToolbar.tsx ← Node-specific tools
└── layouts/
└── EditorLayout.tsx ← Main layout orchestration
🔧 Implementation Roadmap
Phase 1: Extend StudioBridge API (Engine Side)
File: engine/modules/studio_bridge/studio_bridge.h
Add these method declarations:
// Advanced node operations
Dictionary move_node(const Dictionary &p_params);
Dictionary duplicate_node(const Dictionary &p_params);
Dictionary rename_node(const Dictionary &p_params);
// Advanced properties
Dictionary get_all_properties(const Dictionary &p_params);
Dictionary get_property_info(const Dictionary &p_params);
// File system
Dictionary list_directory(const Dictionary &p_params);
Dictionary get_recent_files(const Dictionary &p_params);
// Scripts
Dictionary attach_script(const Dictionary &p_params);
Dictionary get_node_script(const Dictionary &p_params);
// Viewport streaming
Dictionary get_viewport_texture(const Dictionary &p_params);
Dictionary set_camera_transform(const Dictionary &p_params);
File: engine/modules/studio_bridge/studio_bridge.cpp
Implement these methods (similar to existing _handle_create_node, etc.)
Phase 2: Build Studio Bridge Client (Studio Side)
File: studio/src/engine/bridge.ts
export class EngineBridge {
private baseUrl = 'http://localhost:6007';
private ws: WebSocket | null = null;
// Basic API calls
async call(method: string, params: any = {}) {
const response = await fetch(`${this.baseUrl}/rpc`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ method, params })
});
return response.json();
}
// Scene Tree
async loadScene(path: string) {
return this.call('loadScene', { path });
}
async getSceneTree() {
return this.call('getSceneTree', {});
}
async createNode(type: string, parent: string, name: string) {
return this.call('createNode', { type, parent, name });
}
async deleteNode(path: string) {
return this.call('deleteNode', { path });
}
async moveNode(path: string, newParent: string, position: number) {
return this.call('moveNode', { path, newParent, position });
}
async duplicateNode(path: string) {
return this.call('duplicateNode', { path });
}
// Properties
async getAllProperties(path: string) {
return this.call('getAllProperties', { path });
}
async setProperty(path: string, property: string, value: any) {
return this.call('setProperty', { path, property, value });
}
async getProperty(path: string, property: string) {
return this.call('getProperty', { path, property });
}
// File System
async listDirectory(path: string) {
return this.call('listDirectory', { path });
}
// Scripts
async attachScript(nodePath: string, scriptPath: string) {
return this.call('attachScript', { nodePath, scriptPath });
}
// Game Control
async runGame() {
return this.call('runGame', {});
}
async stopGame() {
return this.call('stopGame', {});
}
// WebSocket Events
connectEvents(callbacks: {
onSceneChanged?: () => void;
onNodeSelected?: (node: any) => void;
onPropertyChanged?: (path: string, property: string) => void;
onConsoleOutput?: (message: string, type: string) => void;
}) {
this.ws = new WebSocket('ws://localhost:6007/events');
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event === 'scene_changed' && callbacks.onSceneChanged) {
callbacks.onSceneChanged();
} else if (data.event === 'node_selected' && callbacks.onNodeSelected) {
callbacks.onNodeSelected(data.node);
} else if (data.event === 'property_changed' && callbacks.onPropertyChanged) {
callbacks.onPropertyChanged(data.path, data.property);
} else if (data.event === 'console_output' && callbacks.onConsoleOutput) {
callbacks.onConsoleOutput(data.message, data.type);
}
};
}
}
// Singleton instance
export const bridge = new EngineBridge();
Phase 3: Build React Components (Studio Side)
File: studio/src/components/SceneTree/SceneTreePanel.tsx
import { useState, useEffect } from 'react';
import { bridge } from '@/engine/bridge';
interface Node {
name: string;
type: string;
path: string;
children: Node[];
}
export function SceneTreePanel() {
const [tree, setTree] = useState<Node | null>(null);
const [selectedPath, setSelectedPath] = useState<string>('');
useEffect(() => {
// Load initial scene tree
loadTree();
// Listen for changes
bridge.connectEvents({
onSceneChanged: loadTree,
onNodeSelected: (node) => setSelectedPath(node.path)
});
}, []);
async function loadTree() {
const result = await bridge.getSceneTree();
if (result.success) {
setTree(result.result);
}
}
async function handleCreateNode(type: string, parentPath: string) {
await bridge.createNode(type, parentPath, `New${type}`);
loadTree();
}
async function handleDeleteNode(path: string) {
await bridge.deleteNode(path);
loadTree();
}
async function handleSelectNode(path: string) {
await bridge.call('selectNode', { path });
setSelectedPath(path);
}
return (
<div className="scene-tree-panel">
<div className="toolbar">
<button onClick={() => handleCreateNode('Node2D', selectedPath)}>
+ Add Node
</button>
<button onClick={() => handleDeleteNode(selectedPath)}>
Delete
</button>
</div>
{tree && (
<NodeTree
node={tree}
selectedPath={selectedPath}
onSelect={handleSelectNode}
/>
)}
</div>
);
}
function NodeTree({ node, selectedPath, onSelect }: {
node: Node;
selectedPath: string;
onSelect: (path: string) => void;
}) {
const [expanded, setExpanded] = useState(true);
return (
<div className="node-item">
<div
className={`node-header ${node.path === selectedPath ? 'selected' : ''}`}
onClick={() => onSelect(node.path)}
>
<span onClick={() => setExpanded(!expanded)}>
{node.children.length > 0 ? (expanded ? '▼' : '▶') : ' '}
</span>
<span className="node-icon">{getIconForType(node.type)}</span>
<span className="node-name">{node.name}</span>
<span className="node-type">{node.type}</span>
</div>
{expanded && node.children.length > 0 && (
<div className="node-children">
{node.children.map((child) => (
<NodeTree
key={child.path}
node={child}
selectedPath={selectedPath}
onSelect={onSelect}
/>
))}
</div>
)}
</div>
);
}
function getIconForType(type: string): string {
// Return icon based on node type
const icons: Record<string, string> = {
'Node2D': '🎯',
'Sprite2D': '🖼️',
'Camera2D': '📷',
'CharacterBody2D': '🏃',
'Node3D': '📦',
'MeshInstance3D': '🎲',
// ... add more
};
return icons[type] || '⚫';
}
File: studio/src/components/Inspector/InspectorPanel.tsx
import { useState, useEffect } from 'react';
import { bridge } from '@/engine/bridge';
export function InspectorPanel({ selectedNodePath }: { selectedNodePath: string }) {
const [properties, setProperties] = useState<any[]>([]);
useEffect(() => {
if (selectedNodePath) {
loadProperties();
}
}, [selectedNodePath]);
async function loadProperties() {
const result = await bridge.getAllProperties(selectedNodePath);
if (result.success) {
setProperties(result.result);
}
}
async function handlePropertyChange(property: string, value: any) {
await bridge.setProperty(selectedNodePath, property, value);
}
if (!selectedNodePath) {
return <div className="inspector-empty">No node selected</div>;
}
return (
<div className="inspector-panel">
<h3>Inspector</h3>
<div className="property-list">
{properties.map((prop) => (
<PropertyEditor
key={prop.name}
property={prop}
onChange={(value) => handlePropertyChange(prop.name, value)}
/>
))}
</div>
</div>
);
}
function PropertyEditor({ property, onChange }: any) {
// Render different input types based on property type
if (property.type === 'Vector2') {
return (
<div className="property-vector2">
<label>{property.name}</label>
<input
type="number"
value={property.value.x}
onChange={(e) => onChange({ ...property.value, x: +e.target.value })}
/>
<input
type="number"
value={property.value.y}
onChange={(e) => onChange({ ...property.value, y: +e.target.value })}
/>
</div>
);
}
if (property.type === 'bool') {
return (
<div className="property-bool">
<label>{property.name}</label>
<input
type="checkbox"
checked={property.value}
onChange={(e) => onChange(e.target.checked)}
/>
</div>
);
}
// Default: text input
return (
<div className="property-default">
<label>{property.name}</label>
<input
type="text"
value={property.value}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
}
📊 Feature Checklist
✅ Core (Already Built via StudioBridge)
- Load/save scenes
- Create/delete nodes
- Get/set properties
- Scene tree hierarchy
⏳ Advanced (Need to Add)
- Move/reparent nodes
- Duplicate nodes
- Copy/paste nodes
- Undo/redo system
- Multi-node selection
- Node groups
- Script attachment
- Resource management
- Asset import
- 2D/3D viewport rendering
- Camera controls
- Gizmos (transform handles)
- Debugger integration
- Profiler data
- Remote scene tree (running game)
🚀 Next Steps
- Extend StudioBridge API - Add 20+ new RPC methods for advanced features
- Implement HTTP Server - Make the bridge actually accept network requests
- Build React Components - Create SceneTree, Inspector, Viewport panels
- Add WebSocket Events - Real-time updates Engine → Studio
- Integrate Monaco Editor - For script editing
- Viewport Streaming - Show 3D scene in Studio UI
Estimated Time: 4-6 weeks for full feature parity
Want me to start implementing the extended API methods?