blenderpython/blender_web.py

531 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import bpy
import threading
import http.server
import socketserver
import webbrowser
import os
import tempfile
import json
import time
from pathlib import Path
class BlenderWebServer:
def __init__(self, port=8000):
self.port = port
self.server = None
self.thread = None
self.html_file = None
def create_html_content(self):
"""创建简化的HTML页面内容"""
return """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blender 简单控制面板</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
padding: 40px;
text-align: center;
max-width: 500px;
width: 90%;
}
.header {
margin-bottom: 30px;
}
.header h1 {
font-size: 2em;
color: #2c3e50;
margin-bottom: 10px;
}
.header p {
color: #7f8c8d;
font-size: 1.1em;
}
.button {
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
color: white;
padding: 15px 30px;
border: none;
border-radius: 8px;
cursor: pointer;
margin: 10px;
font-size: 16px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
min-width: 150px;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
}
.button:active {
transform: translateY(0);
}
.button.success {
background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3);
}
.button.success:hover {
box-shadow: 0 6px 20px rgba(39, 174, 96, 0.4);
}
.status {
padding: 15px;
margin: 20px 0;
border-radius: 8px;
background: #ecf0f1;
border-left: 4px solid #3498db;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
text-align: left;
}
.status.success {
background: #d5f4e6;
border-left-color: #27ae60;
color: #27ae60;
}
.status.error {
background: #fadbd8;
border-left-color: #e74c3c;
color: #e74c3c;
}
.footer {
margin-top: 30px;
color: #7f8c8d;
font-size: 14px;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1> Blender 控制面板</h1>
<p>简单的Web控制界面</p>
</div>
<div class="status" id="status">
就绪 - 等待操作
</div>
<div>
<button class="button success" onclick="addCube()">📦 添加立方体</button>
<button class="button" onclick="getSceneInfo()">📊 查看场景信息</button>
</div>
<div class="footer">
<p>端口: """ + str(self.port) + """ | 简化版本</p>
</div>
</div>
<script>
// 更新状态显示
function updateStatus(message, type = '') {
const element = document.getElementById('status');
element.innerHTML = message;
element.className = `status ${type}`;
}
// 显示加载状态
function showLoading() {
updateStatus('<div class="loading"></div>处理中...');
}
// 添加立方体
function addCube() {
showLoading();
fetch('/api/add_cube', {method: 'POST'})
.then(response => response.json())
.then(data => {
if (data.error) {
updateStatus('错误: ' + data.error, 'error');
} else {
updateStatus(data.message, 'success');
}
})
.catch(error => {
updateStatus('网络错误: ' + error, 'error');
});
}
// 获取场景信息
function getSceneInfo() {
showLoading();
fetch('/api/scene_info')
.then(response => response.json())
.then(data => {
if (data.error) {
updateStatus('错误: ' + data.error, 'error');
} else {
const info = `场景: ${data.scene_name}<br>
对象数: ${data.object_count}<br>
材质数: ${data.material_count}<br>
网格数: ${data.mesh_count}<br>
Blender版本: ${data.blender_version}`;
updateStatus(info, 'success');
}
})
.catch(error => {
updateStatus('网络错误: ' + error, 'error');
});
}
// 页面加载时获取场景信息
window.onload = function() {
getSceneInfo();
};
</script>
</body>
</html>
"""
def start_server(self):
"""启动HTTP服务器"""
try:
# 创建HTML文件
temp_dir = tempfile.gettempdir()
self.html_file = os.path.join(temp_dir, "blender_web_panel.html")
with open(self.html_file, 'w', encoding='utf-8') as f:
f.write(self.create_html_content())
# 创建自定义HTTP处理器
class BlenderHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/':
# 修复直接使用服务器的html_file属性
self.path = self.server.html_file
elif self.path.startswith('/api/'):
self.handle_api_request()
return
return http.server.SimpleHTTPRequestHandler.do_GET(self)
def do_POST(self):
if self.path.startswith('/api/'):
self.handle_api_request()
return
return http.server.SimpleHTTPRequestHandler.do_POST(self)
def handle_api_request(self):
"""处理API请求"""
try:
if self.path == '/api/scene_info':
self.send_scene_info()
elif self.path == '/api/add_cube':
self.handle_add_cube()
else:
self.send_error(404, "API not found")
except Exception as e:
self.send_error(500, str(e))
def send_json_response(self, data):
"""发送JSON响应"""
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header(
'Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header(
'Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
self.wfile.write(json.dumps(
data, ensure_ascii=False).encode('utf-8'))
def send_scene_info(self):
"""发送场景信息"""
try:
scene = bpy.context.scene
data = {
'scene_name': scene.name,
'object_count': len(bpy.data.objects),
'material_count': len(bpy.data.materials),
'mesh_count': len(bpy.data.meshes),
'blender_version': bpy.app.version_string
}
self.send_json_response(data)
except Exception as e:
self.send_json_response({'error': str(e)})
def handle_add_cube(self):
"""处理添加立方体请求"""
try:
bpy.ops.mesh.primitive_cube_add()
self.send_json_response({'message': '立方体已添加'})
except Exception as e:
self.send_json_response({'error': str(e)})
# 创建自定义服务器类
class BlenderTCPServer(socketserver.TCPServer):
def __init__(self, server_address, RequestHandlerClass, html_file):
self.html_file = html_file
super().__init__(server_address, RequestHandlerClass)
# 启动服务器
with BlenderTCPServer(("", self.port), BlenderHTTPRequestHandler, self.html_file) as httpd:
self.server = httpd
print(f"🎨 Blender Web服务器启动在端口 {self.port}")
print(f"🌐 访问地址: http://localhost:{self.port}")
httpd.serve_forever()
except Exception as e:
print(f"❌ 服务器启动失败: {e}")
def start(self):
"""在后台线程中启动服务器"""
self.thread = threading.Thread(target=self.start_server, daemon=True)
self.thread.start()
def stop(self):
"""停止服务器"""
if self.server:
self.server.shutdown()
self.server.server_close()
# 全局服务器实例
web_server = None
def start_web_server(port=8000):
"""启动Web服务器"""
global web_server
if web_server is None:
web_server = BlenderWebServer(port)
web_server.start()
print("✅ Web服务器已启动")
return web_server
def open_web_panel():
"""在Blender中打开Web面板"""
# 启动服务器
server = start_web_server()
# 检查Blender版本是否支持WEB_BROWSER
try:
# 尝试在Blender中打开Web浏览器面板
bpy.ops.screen.area_split(direction='VERTICAL', factor=0.5)
# 检查可用的区域类型
available_areas = ('EMPTY', 'VIEW_3D', 'IMAGE_EDITOR', 'NODE_EDITOR',
'SEQUENCE_EDITOR', 'CLIP_EDITOR', 'DOPESHEET_EDITOR',
'GRAPH_EDITOR', 'NLA_EDITOR', 'TEXT_EDITOR', 'CONSOLE',
'INFO', 'TOPBAR', 'STATUSBAR', 'OUTLINER', 'PROPERTIES',
'FILE_BROWSER', 'SPREADSHEET', 'PREFERENCES')
# 尝试使用TEXT_EDITOR作为替代
for area in bpy.context.screen.areas:
if area.type == 'INFO':
area.type = 'TEXT_EDITOR'
# 创建一个简单的HTML显示
text_editor = area.spaces[0]
text_editor.text = bpy.data.texts.new("Web Panel")
text_editor.text.write(f"""
Blender 简单控制面板
服务器已启动在端口 {server.port}
访问地址: http://localhost:{server.port}
功能:
- 添加立方体
- 查看场景信息
请在浏览器中打开上述地址使用Web界面。
""")
break
print(f"🌐 Web面板信息已在文本编辑器中显示")
except Exception as e:
print(f"❌ 在Blender中打开Web面板失败: {e}")
# 如果失败,尝试在外部浏览器中打开
try:
webbrowser.open(f"http://localhost:{server.port}")
print(f" 已在外部浏览器中打开Web面板")
except Exception as e2:
print(f"❌ 打开外部浏览器失败: {e2}")
# Blender操作符类
class StartWebServerOperator(bpy.types.Operator):
bl_idname = "wm.start_web_server"
bl_label = "启动Web服务器"
bl_description = "启动Blender Web控制服务器"
def execute(self, context):
start_web_server()
self.report({'INFO'}, f"Web服务器已启动在端口8000")
return {'FINISHED'}
class OpenWebPanelOperator(bpy.types.Operator):
bl_idname = "wm.open_web_panel"
bl_label = "打开Web面板"
bl_description = "在Blender中打开Web控制面板"
def execute(self, context):
open_web_panel()
return {'FINISHED'}
class StopWebServerOperator(bpy.types.Operator):
bl_idname = "wm.stop_web_server"
bl_label = "停止Web服务器"
bl_description = "停止Blender Web控制服务器"
def execute(self, context):
global web_server
if web_server:
web_server.stop()
web_server = None
self.report({'INFO'}, "Web服务器已停止")
else:
self.report({'WARNING'}, "Web服务器未运行")
return {'FINISHED'}
# 菜单类
class WebPanelMenu(bpy.types.Menu):
bl_idname = "VIEW3D_MT_web_panel"
bl_label = "Web控制面板"
def draw(self, context):
layout = self.layout
layout.operator("wm.start_web_server")
layout.operator("wm.open_web_panel")
layout.operator("wm.stop_web_server")
# 面板类
class WebPanelPanel(bpy.types.Panel):
bl_label = "Web控制面板"
bl_idname = "VIEW3D_PT_web_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Web控制'
def draw(self, context):
layout = self.layout
# 服务器状态
global web_server
if web_server and web_server.server:
layout.label(text="✅ 服务器运行中", icon='CHECKMARK')
layout.label(text=f"端口: {web_server.port}")
else:
layout.label(text="❌ 服务器未运行", icon='ERROR')
# 控制按钮
col = layout.column(align=True)
col.operator("wm.start_web_server", text="启动服务器", icon='PLAY')
col.operator("wm.open_web_panel", text="打开面板", icon='URL')
col.operator("wm.stop_web_server", text="停止服务器", icon='X')
# 注册函数
def register():
bpy.utils.register_class(StartWebServerOperator)
bpy.utils.register_class(OpenWebPanelOperator)
bpy.utils.register_class(StopWebServerOperator)
bpy.utils.register_class(WebPanelMenu)
bpy.utils.register_class(WebPanelPanel)
# 添加到视图菜单
def draw_menu(self, context):
layout = self.layout
layout.menu("VIEW3D_MT_web_panel")
bpy.types.VIEW3D_MT_view.append(draw_menu)
def unregister():
bpy.utils.unregister_class(StartWebServerOperator)
bpy.utils.unregister_class(OpenWebPanelOperator)
bpy.utils.unregister_class(StopWebServerOperator)
bpy.utils.unregister_class(WebPanelMenu)
bpy.utils.unregister_class(WebPanelPanel)
# 从视图菜单移除
bpy.types.VIEW3D_MT_view.remove(draw_menu)
# 主执行函数
def main():
"""主函数 - 一键启动Web服务器和面板"""
print("🚀 启动Blender 简单Web控制面板...")
# 启动服务器
server = start_web_server()
# 等待服务器启动
time.sleep(1)
# 打开Web面板
open_web_panel()
print("✅ Blender 简单Web控制面板启动完成")
print(f"🌐 访问地址: http://localhost:{server.port}")
print(" 提示: 可以在3D视图的侧边栏找到'Web控制'面板")
# 注册所有类
register()
# 如果直接运行脚本,自动启动
if __name__ == "__main__":
main()