531 lines
17 KiB
Python
531 lines
17 KiB
Python
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()
|