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

350
skills/addon/ui.py Normal file
View File

@@ -0,0 +1,350 @@
"""
Blender Toolkit UI Components
UI 패널, 오퍼레이터, 속성 그룹 정의
"""
import asyncio
import threading
from typing import Any
import bpy
from .retargeting import auto_map_bones, retarget_animation
# ============================================================================
# Blender UI Panel
# ============================================================================
class BLENDERTOOLKIT_PT_Panel(bpy.types.Panel):
"""Blender Toolkit 사이드바 패널"""
bl_label = "Blender Toolkit"
bl_idname = "BLENDERTOOLKIT_PT_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Blender Toolkit'
def draw(self, context):
"""UI 패널 그리기."""
layout = self.layout
# 서버 상태 표시
layout.label(text="WebSocket Server", icon='NETWORK_DRIVE')
# 서버 시작/중지 버튼
row = layout.row()
row.operator("blendertoolkit.start_server", text="Start Server", icon='PLAY')
row.operator("blendertoolkit.stop_server", text="Stop Server", icon='PAUSE')
layout.separator()
# 포트 설정
layout.prop(context.scene, "blender_toolkit_port", text="Port")
class BLENDERTOOLKIT_OT_StartServer(bpy.types.Operator):
"""서버 시작 오퍼레이터"""
bl_idname = "blendertoolkit.start_server"
bl_label = "Start WebSocket Server"
def execute(self, context):
"""WebSocket 서버를 시작하는 오퍼레이터 실행."""
# Import here to avoid circular dependency
from . import BlenderWebSocketServer
port = context.scene.blender_toolkit_port
# Check if server is already running
if hasattr(bpy.types.Scene, '_blender_toolkit_server_thread'):
server_thread = bpy.types.Scene._blender_toolkit_server_thread
if server_thread and server_thread.is_alive():
self.report({'INFO'}, f"WebSocket server already running on port {port}")
return {'CANCELLED'}
# Start server in background thread
def run_server():
"""서버를 별도 스레드에서 실행"""
try:
server = BlenderWebSocketServer(port)
# Store server instance globally
bpy.types.Scene._blender_toolkit_server = server
# Create new event loop for this thread
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# Store loop reference for graceful shutdown
bpy.types.Scene._blender_toolkit_event_loop = loop
# Run server until stopped
loop.run_until_complete(server.start())
# Keep loop running until stop() is called
loop.run_forever()
except (RuntimeError, OSError, ValueError) as e:
print(f"Blender Toolkit Server Error: {e}")
finally:
# Cleanup
loop.close()
# Start server thread
server_thread = threading.Thread(target=run_server, daemon=True)
server_thread.start()
# Store thread reference
bpy.types.Scene._blender_toolkit_server_thread = server_thread
bpy.types.Scene._blender_toolkit_server_port = port
self.report({'INFO'}, f"✓ WebSocket server started on port {port}")
print(f"Blender Toolkit: WebSocket server started on ws://127.0.0.1:{port}")
return {'FINISHED'}
class BLENDERTOOLKIT_OT_StopServer(bpy.types.Operator):
"""서버 중지 오퍼레이터"""
bl_idname = "blendertoolkit.stop_server"
bl_label = "Stop WebSocket Server"
def execute(self, context):
"""WebSocket 서버를 중지하는 오퍼레이터 실행."""
# Check if server is running
if not hasattr(bpy.types.Scene, '_blender_toolkit_server_thread'):
self.report({'WARNING'}, "WebSocket server is not running")
return {'CANCELLED'}
server_thread = bpy.types.Scene._blender_toolkit_server_thread
if not server_thread or not server_thread.is_alive():
self.report({'WARNING'}, "WebSocket server is not running")
# Clean up stale references
self._cleanup_references()
return {'CANCELLED'}
# Get server instance and event loop
server = getattr(bpy.types.Scene, '_blender_toolkit_server', None)
loop = getattr(bpy.types.Scene, '_blender_toolkit_event_loop', None)
port = getattr(bpy.types.Scene, '_blender_toolkit_server_port', 'unknown')
if not server or not loop:
self.report({'WARNING'}, "Server instance not found. Please restart Blender to fully stop the server.")
self._cleanup_references()
return {'CANCELLED'}
try:
# Schedule server stop in the event loop thread
def stop_server():
"""이벤트 루프에서 서버를 안전하게 종료합니다."""
async def _shutdown():
try:
# 서버의 비동기 중지 메서드를 호출하고 완료될 때까지 기다립니다.
await server.stop()
except (RuntimeError, ValueError) as e:
print(f"Error stopping server: {e}")
finally:
# 서버가 완전히 중지된 후에 이벤트 루프를 멈춥니다.
loop.stop()
# 스레드 안전하게 비동기 종료 시퀀스를 스케줄링합니다.
asyncio.ensure_future(_shutdown())
# Call stop_server() in the event loop thread (thread-safe)
loop.call_soon_threadsafe(stop_server)
self.report({'INFO'}, f"✓ WebSocket server on port {port} stopped successfully")
print(f"Blender Toolkit: WebSocket server on port {port} stopped")
except (RuntimeError, ValueError, AttributeError) as e:
self.report({'ERROR'}, f"Failed to stop server: {str(e)}")
print(f"Blender Toolkit: Error stopping server: {e}")
return {'CANCELLED'}
finally:
# Clean up all references
self._cleanup_references()
return {'FINISHED'}
def _cleanup_references(self):
"""Clean up all server references"""
if hasattr(bpy.types.Scene, '_blender_toolkit_server'):
delattr(bpy.types.Scene, '_blender_toolkit_server')
if hasattr(bpy.types.Scene, '_blender_toolkit_event_loop'):
delattr(bpy.types.Scene, '_blender_toolkit_event_loop')
if hasattr(bpy.types.Scene, '_blender_toolkit_server_thread'):
delattr(bpy.types.Scene, '_blender_toolkit_server_thread')
if hasattr(bpy.types.Scene, '_blender_toolkit_server_port'):
delattr(bpy.types.Scene, '_blender_toolkit_server_port')
class BLENDERTOOLKIT_PT_BoneMappingPanel(bpy.types.Panel):
"""본 매핑 리뷰 패널"""
bl_label = "Bone Mapping Review"
bl_idname = "BLENDERTOOLKIT_PT_bone_mapping"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Blender Toolkit'
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
"""본 매핑 리뷰 패널 그리기."""
layout = self.layout
scene = context.scene
# Show armature info
if scene.bone_mapping_source_armature and scene.bone_mapping_target_armature:
layout.label(text=f"Source: {scene.bone_mapping_source_armature}", icon='ARMATURE_DATA')
layout.label(text=f"Target: {scene.bone_mapping_target_armature}", icon='ARMATURE_DATA')
layout.separator()
# Bone mapping table
if len(scene.bone_mapping_items) > 0:
mapping_count = len(scene.bone_mapping_items)
layout.label(
text=f"Bone Mappings ({mapping_count}):", icon='BONE_DATA'
)
# Header
box = layout.box()
row = box.row()
row.label(text="Source Bone")
row.label(text="")
row.label(text="Target Bone")
# Mapping items (scrollable)
for _, item in enumerate(scene.bone_mapping_items):
row = layout.row()
row.label(text=item.source_bone)
row.label(text="")
# Editable target bone dropdown
target_armature = bpy.data.objects.get(scene.bone_mapping_target_armature)
if target_armature and target_armature.type == 'ARMATURE':
row.prop_search(item, "target_bone", target_armature.data, "bones", text="")
else:
row.prop(item, "target_bone", text="")
layout.separator()
# Auto re-map button
layout.operator(
"blendertoolkit.auto_remap", text="Auto Re-map",
icon='FILE_REFRESH'
)
# Apply retargeting button
layout.separator()
# Show status
if hasattr(scene, 'bone_mapping_status'):
if scene.bone_mapping_status == "APPLYING":
layout.label(text="⏳ Applying retargeting...", icon='TIME')
elif scene.bone_mapping_status == "COMPLETED":
layout.label(text="✓ Retargeting completed!", icon='CHECKMARK')
elif scene.bone_mapping_status == "FAILED":
layout.label(text="✗ Retargeting failed", icon='ERROR')
layout.operator(
"blendertoolkit.apply_retargeting",
text="Apply Retargeting", icon='PLAY'
)
else:
layout.label(text="No bone mapping data", icon='INFO')
else:
layout.label(text="No bone mapping loaded", icon='INFO')
layout.label(text="Waiting for Claude Code...", icon='TIME')
class BLENDERTOOLKIT_OT_AutoRemap(bpy.types.Operator):
"""자동 재매핑 오퍼레이터"""
bl_idname = "blendertoolkit.auto_remap"
bl_label = "Auto Re-map Bones"
bl_description = "Re-generate bone mapping automatically"
def execute(self, context):
"""자동 본 재매핑 오퍼레이터 실행."""
scene = context.scene
source_armature = scene.bone_mapping_source_armature
target_armature = scene.bone_mapping_target_armature
if not source_armature or not target_armature:
self.report({'ERROR'}, "Source or target armature not set")
return {'CANCELLED'}
try:
# Auto-map bones
bone_map = auto_map_bones(source_armature, target_armature)
# Update scene properties
scene.bone_mapping_items.clear()
for source_bone, target_bone in bone_map.items():
item = scene.bone_mapping_items.add()
item.source_bone = source_bone
item.target_bone = target_bone
self.report({'INFO'}, f"Re-mapped {len(bone_map)} bones")
except (ValueError, KeyError, AttributeError) as e:
self.report({'ERROR'}, f"Auto re-mapping failed: {str(e)}")
return {'CANCELLED'}
return {'FINISHED'}
class BLENDERTOOLKIT_OT_ApplyRetargeting(bpy.types.Operator):
"""리타게팅 적용 오퍼레이터"""
bl_idname = "blendertoolkit.apply_retargeting"
bl_label = "Apply Retargeting"
bl_description = "Apply retargeting with current bone mapping"
def execute(self, context):
"""애니메이션 리타게팅을 적용하는 오퍼레이터 실행."""
scene = context.scene
source_armature = scene.bone_mapping_source_armature
target_armature = scene.bone_mapping_target_armature
if not source_armature or not target_armature:
self.report({'ERROR'}, "Source or target armature not set")
return {'CANCELLED'}
# Build bone map from items
bone_map = {}
for item in scene.bone_mapping_items:
if item.target_bone: # Skip empty mappings
bone_map[item.source_bone] = item.target_bone
if not bone_map:
self.report({'ERROR'}, "No bone mappings defined")
return {'CANCELLED'}
try:
# Show progress in UI
self.report({'INFO'}, f"Applying retargeting to {len(bone_map)} bones...")
# Set status flag
scene.bone_mapping_status = "APPLYING"
result = retarget_animation(
source_armature,
target_armature,
bone_map,
preserve_rotation=True,
preserve_location=True
)
# Update status
scene.bone_mapping_status = "COMPLETED"
self.report({'INFO'}, result)
except (ValueError, KeyError, AttributeError, RuntimeError) as e:
scene.bone_mapping_status = "FAILED"
self.report({'ERROR'}, f"Retargeting failed: {str(e)}")
return {'CANCELLED'}
return {'FINISHED'}
# ============================================================================
# Property Groups
# ============================================================================
class BoneMappingItem(bpy.types.PropertyGroup):
"""본 매핑 아이템"""
source_bone: bpy.props.StringProperty(name="Source Bone")
target_bone: bpy.props.StringProperty(name="Target Bone")