Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:18:51 +08:00
commit d80558b1cf
52 changed files with 12920 additions and 0 deletions

View File

@@ -0,0 +1,485 @@
"""
WebSocket Server for Blender Toolkit
Claude Code와 통신하기 위한 WebSocket 서버
이 모듈은 Blender 내부에서 WebSocket 서버를 실행하여
외부 클라이언트(Claude Code)와 JSON-RPC 스타일 통신을 제공합니다.
"""
import asyncio
import json
from typing import Any, Dict, Union
import bpy
from aiohttp import web
from aiohttp.web import Request, WebSocketResponse
from .utils.logger import get_logger
from .utils.security import validate_port
# 모듈 로거 초기화
logger = get_logger('websocket_server')
# 보안 상수
MAX_CONNECTIONS = 5 # 최대 동시 연결 수 (로컬 환경)
class BlenderWebSocketServer:
"""WebSocket 서버 메인 클래스"""
def __init__(self, port: int = 9400):
self.port = validate_port(port)
self.app = None
self.runner = None
self.site = None
self.clients = []
async def handle_command(
self, request: Request
) -> Union[WebSocketResponse, web.Response]:
"""WebSocket 연결 핸들러"""
# 로컬호스트만 허용 (보안)
peername = request.transport.get_extra_info('peername')
if peername:
host = peername[0]
if host not in ('127.0.0.1', '::1', 'localhost'):
logger.warning(
"Rejected connection from non-localhost: %s", host
)
return web.Response(
status=403, text="Only localhost connections allowed"
)
# 최대 연결 수 제한 (DoS 방지)
if len(self.clients) >= MAX_CONNECTIONS:
logger.warning(
"Connection limit reached (%d)", MAX_CONNECTIONS
)
return web.Response(status=503, text="Too many connections")
ws = web.WebSocketResponse()
await ws.prepare(request)
self.clients.append(ws)
logger.info("Client connected (total: %d)", len(self.clients))
print(f"✅ Client connected (total: {len(self.clients)})")
async for msg in ws:
if msg.type == web.WSMsgType.TEXT:
try:
data = json.loads(msg.data)
logger.debug("Received message: %s", data)
response = await self.process_command(data)
await ws.send_json(response)
except (json.JSONDecodeError, KeyError, ValueError) as e:
logger.error("Error handling message: %s", e, exc_info=True)
await ws.send_json({
"id": data.get("id") if 'data' in locals() else None,
"error": {
"code": -1,
"message": str(e)
}
})
elif msg.type == web.WSMsgType.ERROR:
logger.error('WebSocket error: %s', ws.exception())
print(f'❌ WebSocket error: {ws.exception()}')
self.clients.remove(ws)
logger.info("Client disconnected (total: %d)", len(self.clients))
print(f"🔌 Client disconnected (total: {len(self.clients)})")
return ws
async def process_command(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""명령 처리"""
method = data.get("method")
params = data.get("params", {})
msg_id = data.get("id")
logger.info("Processing command: %s", method)
logger.debug("Command params: %s", params)
print(f"📨 Received command: {method}")
try:
# 메서드 라우팅
if method.startswith("Armature."):
result = await self.handle_armature_command(method, params)
elif method.startswith("Retargeting."):
result = await self.handle_retargeting_command(method, params)
elif method.startswith("BoneMapping."):
result = await self.handle_bonemapping_command(method, params)
elif method.startswith("Animation."):
result = await self.handle_animation_command(method, params)
elif method.startswith("Import."):
result = await self.handle_import_command(method, params)
elif method.startswith("Geometry."):
result = await self.handle_geometry_command(method, params)
elif method.startswith("Object."):
result = await self.handle_object_command(method, params)
elif method.startswith("Modifier."):
result = await self.handle_modifier_command(method, params)
elif method.startswith("Material."):
result = await self.handle_material_command(method, params)
elif method.startswith("Collection."):
result = await self.handle_collection_command(method, params)
else:
raise ValueError(f"Unknown method: {method}")
logger.info("Command %s completed successfully", method)
return {"id": msg_id, "result": result}
except (ValueError, KeyError, AttributeError, RuntimeError) as e:
logger.error("Error processing %s: %s", method, str(e), exc_info=True)
print(f"❌ Error processing {method}: {str(e)}")
return {
"id": msg_id,
"error": {"code": -1, "message": str(e)}
}
async def handle_armature_command(self, method: str, params: Dict) -> Any:
"""아마추어 관련 명령 처리"""
from .retargeting import get_bones, list_armatures
if method == "Armature.getBones":
armature_name = params.get("armatureName")
return get_bones(armature_name)
elif method == "Armature.list":
return list_armatures()
else:
raise ValueError(f"Unknown armature method: {method}")
async def handle_retargeting_command(self, method: str, params: Dict) -> Any:
"""리타게팅 명령 처리"""
from .retargeting import auto_map_bones, retarget_animation, get_preset_bone_mapping
if method == "Retargeting.autoMapBones":
return auto_map_bones(
params.get("sourceArmature"),
params.get("targetArmature")
)
elif method == "Retargeting.retargetAnimation":
return retarget_animation(
params.get("sourceArmature"),
params.get("targetArmature"),
params.get("boneMap"),
params.get("preserveRotation", True),
params.get("preserveLocation", False)
)
elif method == "Retargeting.getPresetMapping":
preset = params.get("preset")
return get_preset_bone_mapping(preset)
else:
raise ValueError(f"Unknown retargeting method: {method}")
async def handle_bonemapping_command(self, method: str, params: Dict) -> Any:
"""본 매핑 명령 처리"""
from .retargeting import store_bone_mapping, load_bone_mapping
if method == "BoneMapping.show":
return store_bone_mapping(
params.get("sourceArmature"),
params.get("targetArmature"),
params.get("boneMapping")
)
elif method == "BoneMapping.get":
return load_bone_mapping(
params.get("sourceArmature"),
params.get("targetArmature")
)
else:
raise ValueError(f"Unknown bone mapping method: {method}")
async def handle_animation_command(self, method: str, params: Dict) -> Any:
"""애니메이션 명령 처리"""
from .retargeting import list_animations, play_animation, stop_animation, add_to_nla
if method == "Animation.list":
armature_name = params.get("armatureName")
return list_animations(armature_name)
elif method == "Animation.play":
return play_animation(
params.get("armatureName"),
params.get("actionName"),
params.get("loop", True)
)
elif method == "Animation.stop":
return stop_animation()
elif method == "Animation.addToNLA":
return add_to_nla(
params.get("armatureName"),
params.get("actionName"),
params.get("trackName")
)
else:
raise ValueError(f"Unknown animation method: {method}")
async def handle_import_command(self, method: str, params: Dict) -> Any:
"""임포트 명령 처리"""
from .retargeting import import_fbx, import_dae
if method == "Import.fbx":
return import_fbx(params.get("filepath"))
elif method == "Import.dae":
return import_dae(params.get("filepath"))
else:
raise ValueError(f"Unknown import method: {method}")
async def handle_geometry_command(self, method: str, params: Dict) -> Any:
"""도형 생성 명령 처리"""
from .commands.geometry import (
create_cube, create_sphere, create_cylinder,
create_plane, create_cone, create_torus,
get_vertices, move_vertex, subdivide_mesh, extrude_face
)
if method == "Geometry.createCube":
return create_cube(
location=tuple(params.get("location", [0, 0, 0])),
size=params.get("size", 2.0),
name=params.get("name")
)
elif method == "Geometry.createSphere":
return create_sphere(
location=tuple(params.get("location", [0, 0, 0])),
radius=params.get("radius", 1.0),
segments=params.get("segments", 32),
ring_count=params.get("ringCount", 16),
name=params.get("name")
)
elif method == "Geometry.createCylinder":
return create_cylinder(
location=tuple(params.get("location", [0, 0, 0])),
radius=params.get("radius", 1.0),
depth=params.get("depth", 2.0),
vertices=params.get("vertices", 32),
name=params.get("name")
)
elif method == "Geometry.createPlane":
return create_plane(
location=tuple(params.get("location", [0, 0, 0])),
size=params.get("size", 2.0),
name=params.get("name")
)
elif method == "Geometry.createCone":
return create_cone(
location=tuple(params.get("location", [0, 0, 0])),
radius1=params.get("radius1", 1.0),
depth=params.get("depth", 2.0),
vertices=params.get("vertices", 32),
name=params.get("name")
)
elif method == "Geometry.createTorus":
return create_torus(
location=tuple(params.get("location", [0, 0, 0])),
major_radius=params.get("majorRadius", 1.0),
minor_radius=params.get("minorRadius", 0.25),
major_segments=params.get("majorSegments", 48),
minor_segments=params.get("minorSegments", 12),
name=params.get("name")
)
elif method == "Geometry.getVertices":
return get_vertices(params.get("name"))
elif method == "Geometry.moveVertex":
return move_vertex(
object_name=params.get("objectName"),
vertex_index=params.get("vertexIndex"),
new_position=tuple(params.get("newPosition"))
)
elif method == "Geometry.subdivideMesh":
return subdivide_mesh(
name=params.get("name"),
cuts=params.get("cuts", 1)
)
elif method == "Geometry.extrudeFace":
return extrude_face(
object_name=params.get("objectName"),
face_index=params.get("faceIndex"),
offset=params.get("offset", 1.0)
)
else:
raise ValueError(f"Unknown geometry method: {method}")
async def handle_object_command(self, method: str, params: Dict) -> Any:
"""오브젝트 명령 처리"""
from .commands.geometry import (
delete_object, transform_object, duplicate_object, list_objects
)
if method == "Object.delete":
return delete_object(params.get("name"))
elif method == "Object.transform":
location = params.get("location")
rotation = params.get("rotation")
scale = params.get("scale")
return transform_object(
name=params.get("name"),
location=tuple(location) if location else None,
rotation=tuple(rotation) if rotation else None,
scale=tuple(scale) if scale else None
)
elif method == "Object.duplicate":
location = params.get("location")
return duplicate_object(
name=params.get("name"),
new_name=params.get("newName"),
location=tuple(location) if location else None
)
elif method == "Object.list":
return list_objects(params.get("type"))
else:
raise ValueError(f"Unknown object method: {method}")
async def handle_modifier_command(self, method: str, params: Dict) -> Any:
"""모디파이어 명령 처리"""
from .commands.modifier import (
add_modifier, apply_modifier, list_modifiers, remove_modifier,
toggle_modifier, modify_modifier_properties, get_modifier_info, reorder_modifier
)
if method == "Modifier.add":
properties = params.get("properties", {})
return add_modifier(
object_name=params.get("objectName"),
modifier_type=params.get("modifierType"),
name=params.get("name")
)
elif method == "Modifier.apply":
return apply_modifier(
object_name=params.get("objectName"),
modifier_name=params.get("modifierName")
)
elif method == "Modifier.list":
return list_modifiers(
object_name=params.get("objectName")
)
elif method == "Modifier.remove":
return remove_modifier(
object_name=params.get("objectName"),
modifier_name=params.get("modifierName")
)
elif method == "Modifier.toggle":
return toggle_modifier(
object_name=params.get("objectName"),
modifier_name=params.get("modifierName"),
viewport=params.get("viewport"),
render=params.get("render")
)
elif method == "Modifier.modify":
properties = params.get("properties", {})
return modify_modifier_properties(
object_name=params.get("objectName"),
modifier_name=params.get("modifierName"),
**properties
)
elif method == "Modifier.getInfo":
return get_modifier_info(
object_name=params.get("objectName"),
modifier_name=params.get("modifierName")
)
elif method == "Modifier.reorder":
return reorder_modifier(
object_name=params.get("objectName"),
modifier_name=params.get("modifierName"),
direction=params.get("direction")
)
else:
raise ValueError(f"Unknown modifier method: {method}")
async def handle_material_command(self, method: str, params: Dict) -> Any:
"""머티리얼 명령 처리"""
from .commands.material import (
create_material, list_materials, delete_material,
assign_material, list_object_materials,
set_material_base_color, set_material_metallic, set_material_roughness,
set_material_emission, get_material_properties
)
if method == "Material.create":
return create_material(
name=params.get("name"),
use_nodes=params.get("useNodes", True)
)
elif method == "Material.list":
return list_materials()
elif method == "Material.delete":
return delete_material(name=params.get("name"))
elif method == "Material.assign":
return assign_material(
object_name=params.get("objectName"),
material_name=params.get("materialName"),
slot_index=params.get("slotIndex", 0)
)
elif method == "Material.listObjectMaterials":
return list_object_materials(object_name=params.get("objectName"))
elif method == "Material.setBaseColor":
color = params.get("color")
return set_material_base_color(
material_name=params.get("materialName"),
color=tuple(color) if isinstance(color, list) else color
)
elif method == "Material.setMetallic":
return set_material_metallic(
material_name=params.get("materialName"),
metallic=params.get("metallic")
)
elif method == "Material.setRoughness":
return set_material_roughness(
material_name=params.get("materialName"),
roughness=params.get("roughness")
)
elif method == "Material.setEmission":
color = params.get("color")
return set_material_emission(
material_name=params.get("materialName"),
color=tuple(color) if isinstance(color, list) else color,
strength=params.get("strength", 1.0)
)
elif method == "Material.getProperties":
return get_material_properties(material_name=params.get("materialName"))
else:
raise ValueError(f"Unknown material method: {method}")
async def handle_collection_command(self, method: str, params: Dict) -> Any:
"""컬렉션 명령 처리"""
from .commands.collection import (
create_collection, list_collections, add_to_collection,
remove_from_collection, delete_collection
)
if method == "Collection.create":
return create_collection(name=params.get("name"))
elif method == "Collection.list":
return list_collections()
elif method == "Collection.addObject":
return add_to_collection(
object_name=params.get("objectName"),
collection_name=params.get("collectionName")
)
elif method == "Collection.removeObject":
return remove_from_collection(
object_name=params.get("objectName"),
collection_name=params.get("collectionName")
)
elif method == "Collection.delete":
return delete_collection(name=params.get("name"))
else:
raise ValueError(f"Unknown collection method: {method}")
async def start(self):
"""서버 시작"""
self.app = web.Application()
self.app.router.add_get('/ws', self.handle_command)
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = web.TCPSite(self.runner, '127.0.0.1', self.port)
await self.site.start()
print(f"✅ Blender WebSocket Server started on port {self.port}")
async def stop(self):
"""서버 중지"""
if self.site:
await self.site.stop()
if self.runner:
await self.runner.cleanup()
print("🛑 Blender WebSocket Server stopped")