545 lines
15 KiB
Markdown
545 lines
15 KiB
Markdown
# 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)
|
|
```cpp
|
|
// 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
|
|
```cpp
|
|
// 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
|
|
```cpp
|
|
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
|
|
```cpp
|
|
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
|
|
```cpp
|
|
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
|
|
```cpp
|
|
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:
|
|
```cpp
|
|
// 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`
|
|
|
|
```typescript
|
|
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`
|
|
|
|
```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`
|
|
|
|
```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)
|
|
- [x] Load/save scenes
|
|
- [x] Create/delete nodes
|
|
- [x] Get/set properties
|
|
- [x] 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
|
|
|
|
1. **Extend StudioBridge API** - Add 20+ new RPC methods for advanced features
|
|
2. **Implement HTTP Server** - Make the bridge actually accept network requests
|
|
3. **Build React Components** - Create SceneTree, Inspector, Viewport panels
|
|
4. **Add WebSocket Events** - Real-time updates Engine → Studio
|
|
5. **Integrate Monaco Editor** - For script editing
|
|
6. **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?
|