389 lines
14 KiB
Python
389 lines
14 KiB
Python
"""
|
|
Modifier Operations
|
|
모디파이어 관리 명령 핸들러
|
|
"""
|
|
|
|
import bpy
|
|
from typing import Dict, List, Any, Optional
|
|
from ..utils.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
def add_modifier(object_name: str, modifier_type: str, name: Optional[str] = None) -> Dict[str, Any]:
|
|
"""오브젝트에 모디파이어 추가
|
|
|
|
Args:
|
|
object_name: 대상 오브젝트 이름
|
|
modifier_type: 모디파이어 타입 (SUBSURF, MIRROR, ARRAY, BEVEL, etc.)
|
|
name: 모디파이어 이름 (optional)
|
|
|
|
Supported modifier types:
|
|
- SUBSURF: Subdivision Surface
|
|
- MIRROR: Mirror
|
|
- ARRAY: Array
|
|
- BEVEL: Bevel
|
|
- BOOLEAN: Boolean
|
|
- SOLIDIFY: Solidify
|
|
- WIREFRAME: Wireframe
|
|
- SKIN: Skin
|
|
- ARMATURE: Armature
|
|
- LATTICE: Lattice
|
|
- CURVE: Curve
|
|
- SIMPLE_DEFORM: Simple Deform
|
|
- CAST: Cast
|
|
- DISPLACE: Displace
|
|
- HOOK: Hook
|
|
- LAPLACIANDEFORM: Laplacian Deform
|
|
- MESH_DEFORM: Mesh Deform
|
|
- SHRINKWRAP: Shrinkwrap
|
|
- WAVE: Wave
|
|
- OCEAN: Ocean
|
|
- PARTICLE_SYSTEM: Particle System
|
|
- CLOTH: Cloth
|
|
- COLLISION: Collision
|
|
- DYNAMIC_PAINT: Dynamic Paint
|
|
- EXPLODE: Explode
|
|
- FLUID: Fluid
|
|
- SOFT_BODY: Soft Body
|
|
"""
|
|
logger.info(f"Adding modifier '{modifier_type}' to '{object_name}'")
|
|
|
|
obj = bpy.data.objects.get(object_name)
|
|
if not obj:
|
|
raise ValueError(f"Object '{object_name}' not found")
|
|
|
|
mod = obj.modifiers.new(name or modifier_type, modifier_type)
|
|
|
|
return {
|
|
'name': mod.name,
|
|
'type': mod.type,
|
|
'show_viewport': mod.show_viewport,
|
|
'show_render': mod.show_render
|
|
}
|
|
|
|
|
|
def apply_modifier(object_name: str, modifier_name: str) -> Dict[str, str]:
|
|
"""모디파이어 적용"""
|
|
logger.info(f"Applying modifier '{modifier_name}' on '{object_name}'")
|
|
|
|
obj = bpy.data.objects.get(object_name)
|
|
if not obj:
|
|
raise ValueError(f"Object '{object_name}' not found")
|
|
|
|
mod = obj.modifiers.get(modifier_name)
|
|
if not mod:
|
|
raise ValueError(f"Modifier '{modifier_name}' not found on '{object_name}'")
|
|
|
|
# 모디파이어 적용은 Edit 모드에서는 할 수 없음
|
|
if bpy.context.mode != 'OBJECT':
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
# 오브젝트 선택 및 활성화
|
|
bpy.context.view_layer.objects.active = obj
|
|
obj.select_set(True)
|
|
|
|
# 모디파이어 적용
|
|
bpy.ops.object.modifier_apply(modifier=modifier_name)
|
|
|
|
return {'status': 'success', 'message': f"Applied modifier '{modifier_name}' to '{object_name}'"}
|
|
|
|
|
|
def list_modifiers(object_name: str) -> List[Dict[str, Any]]:
|
|
"""오브젝트의 모디파이어 목록 조회"""
|
|
logger.info(f"Listing modifiers for '{object_name}'")
|
|
|
|
obj = bpy.data.objects.get(object_name)
|
|
if not obj:
|
|
raise ValueError(f"Object '{object_name}' not found")
|
|
|
|
modifiers = []
|
|
for mod in obj.modifiers:
|
|
mod_info = {
|
|
'name': mod.name,
|
|
'type': mod.type,
|
|
'show_viewport': mod.show_viewport,
|
|
'show_render': mod.show_render,
|
|
}
|
|
|
|
# 타입별 특화 속성 추가
|
|
if mod.type == 'SUBSURF':
|
|
mod_info['levels'] = mod.levels
|
|
mod_info['render_levels'] = mod.render_levels
|
|
elif mod.type == 'MIRROR':
|
|
mod_info['use_axis'] = [mod.use_axis[0], mod.use_axis[1], mod.use_axis[2]]
|
|
mod_info['use_bisect_axis'] = [mod.use_bisect_axis[0], mod.use_bisect_axis[1], mod.use_bisect_axis[2]]
|
|
elif mod.type == 'ARRAY':
|
|
mod_info['count'] = mod.count
|
|
mod_info['use_relative_offset'] = mod.use_relative_offset
|
|
mod_info['relative_offset_displace'] = list(mod.relative_offset_displace)
|
|
elif mod.type == 'BEVEL':
|
|
mod_info['width'] = mod.width
|
|
mod_info['segments'] = mod.segments
|
|
mod_info['limit_method'] = mod.limit_method
|
|
elif mod.type == 'BOOLEAN':
|
|
mod_info['operation'] = mod.operation
|
|
mod_info['object'] = mod.object.name if mod.object else None
|
|
elif mod.type == 'SOLIDIFY':
|
|
mod_info['thickness'] = mod.thickness
|
|
mod_info['offset'] = mod.offset
|
|
elif mod.type == 'ARMATURE':
|
|
mod_info['object'] = mod.object.name if mod.object else None
|
|
mod_info['use_vertex_groups'] = mod.use_vertex_groups
|
|
elif mod.type == 'LATTICE':
|
|
mod_info['object'] = mod.object.name if mod.object else None
|
|
elif mod.type == 'CURVE':
|
|
mod_info['object'] = mod.object.name if mod.object else None
|
|
elif mod.type == 'SIMPLE_DEFORM':
|
|
mod_info['deform_method'] = mod.deform_method
|
|
mod_info['factor'] = mod.factor
|
|
elif mod.type == 'CAST':
|
|
mod_info['cast_type'] = mod.cast_type
|
|
mod_info['factor'] = mod.factor
|
|
elif mod.type == 'DISPLACE':
|
|
mod_info['strength'] = mod.strength
|
|
mod_info['direction'] = mod.direction
|
|
elif mod.type == 'WAVE':
|
|
mod_info['time_offset'] = mod.time_offset
|
|
mod_info['height'] = mod.height
|
|
|
|
modifiers.append(mod_info)
|
|
|
|
return modifiers
|
|
|
|
|
|
def remove_modifier(object_name: str, modifier_name: str) -> Dict[str, str]:
|
|
"""모디파이어 제거"""
|
|
logger.info(f"Removing modifier '{modifier_name}' from '{object_name}'")
|
|
|
|
obj = bpy.data.objects.get(object_name)
|
|
if not obj:
|
|
raise ValueError(f"Object '{object_name}' not found")
|
|
|
|
mod = obj.modifiers.get(modifier_name)
|
|
if not mod:
|
|
raise ValueError(f"Modifier '{modifier_name}' not found on '{object_name}'")
|
|
|
|
obj.modifiers.remove(mod)
|
|
|
|
return {'status': 'success', 'message': f"Removed modifier '{modifier_name}' from '{object_name}'"}
|
|
|
|
|
|
def toggle_modifier(object_name: str, modifier_name: str,
|
|
viewport: Optional[bool] = None,
|
|
render: Optional[bool] = None) -> Dict[str, Any]:
|
|
"""모디파이어 활성화/비활성화
|
|
|
|
Args:
|
|
object_name: 대상 오브젝트 이름
|
|
modifier_name: 모디파이어 이름
|
|
viewport: 뷰포트 표시 on/off (None이면 토글)
|
|
render: 렌더 표시 on/off (None이면 토글)
|
|
"""
|
|
logger.info(f"Toggling modifier '{modifier_name}' on '{object_name}'")
|
|
|
|
obj = bpy.data.objects.get(object_name)
|
|
if not obj:
|
|
raise ValueError(f"Object '{object_name}' not found")
|
|
|
|
mod = obj.modifiers.get(modifier_name)
|
|
if not mod:
|
|
raise ValueError(f"Modifier '{modifier_name}' not found on '{object_name}'")
|
|
|
|
if viewport is not None:
|
|
mod.show_viewport = viewport
|
|
else:
|
|
mod.show_viewport = not mod.show_viewport
|
|
|
|
if render is not None:
|
|
mod.show_render = render
|
|
else:
|
|
mod.show_render = not mod.show_render
|
|
|
|
return {
|
|
'name': mod.name,
|
|
'show_viewport': mod.show_viewport,
|
|
'show_render': mod.show_render
|
|
}
|
|
|
|
|
|
def modify_modifier_properties(object_name: str, modifier_name: str, **properties) -> Dict[str, Any]:
|
|
"""모디파이어 속성 수정
|
|
|
|
Args:
|
|
object_name: 대상 오브젝트 이름
|
|
modifier_name: 모디파이어 이름
|
|
**properties: 수정할 속성들 (key=value 형태)
|
|
|
|
Example properties by modifier type:
|
|
SUBSURF: levels, render_levels
|
|
MIRROR: use_axis, use_bisect_axis, mirror_object
|
|
ARRAY: count, relative_offset_displace
|
|
BEVEL: width, segments, limit_method
|
|
BOOLEAN: operation, object
|
|
SOLIDIFY: thickness, offset
|
|
ARMATURE: object, use_vertex_groups
|
|
SIMPLE_DEFORM: deform_method, factor, angle
|
|
CAST: cast_type, factor, radius
|
|
DISPLACE: strength, direction
|
|
"""
|
|
logger.info(f"Modifying properties of '{modifier_name}' on '{object_name}'")
|
|
|
|
obj = bpy.data.objects.get(object_name)
|
|
if not obj:
|
|
raise ValueError(f"Object '{object_name}' not found")
|
|
|
|
mod = obj.modifiers.get(modifier_name)
|
|
if not mod:
|
|
raise ValueError(f"Modifier '{modifier_name}' not found on '{object_name}'")
|
|
|
|
updated_properties = {}
|
|
for key, value in properties.items():
|
|
if hasattr(mod, key):
|
|
# 특수 처리가 필요한 속성들
|
|
if key in ['use_axis', 'use_bisect_axis'] and isinstance(value, list):
|
|
# Mirror 모디파이어의 axis는 boolean 배열
|
|
for i, v in enumerate(value):
|
|
if i < 3:
|
|
getattr(mod, key)[i] = v
|
|
elif key == 'relative_offset_displace' and isinstance(value, list):
|
|
# Array 모디파이어의 offset은 Vector
|
|
for i, v in enumerate(value):
|
|
if i < 3:
|
|
mod.relative_offset_displace[i] = v
|
|
elif key == 'object' and isinstance(value, str):
|
|
# 오브젝트 참조는 문자열로 받아서 변환
|
|
target_obj = bpy.data.objects.get(value)
|
|
if target_obj:
|
|
setattr(mod, key, target_obj)
|
|
else:
|
|
logger.warn(f"Target object '{value}' not found for property '{key}'")
|
|
continue
|
|
else:
|
|
# 일반 속성
|
|
setattr(mod, key, value)
|
|
|
|
updated_properties[key] = value
|
|
else:
|
|
logger.warn(f"Property '{key}' not found on modifier '{modifier_name}'")
|
|
|
|
return {
|
|
'name': mod.name,
|
|
'type': mod.type,
|
|
'updated_properties': updated_properties
|
|
}
|
|
|
|
|
|
def get_modifier_info(object_name: str, modifier_name: str) -> Dict[str, Any]:
|
|
"""특정 모디파이어의 상세 정보 조회"""
|
|
logger.info(f"Getting info for modifier '{modifier_name}' on '{object_name}'")
|
|
|
|
obj = bpy.data.objects.get(object_name)
|
|
if not obj:
|
|
raise ValueError(f"Object '{object_name}' not found")
|
|
|
|
mod = obj.modifiers.get(modifier_name)
|
|
if not mod:
|
|
raise ValueError(f"Modifier '{modifier_name}' not found on '{object_name}'")
|
|
|
|
# 모든 읽기 가능한 속성을 추출
|
|
info = {
|
|
'name': mod.name,
|
|
'type': mod.type,
|
|
'show_viewport': mod.show_viewport,
|
|
'show_render': mod.show_render,
|
|
}
|
|
|
|
# 타입별 모든 관련 속성 추가
|
|
if mod.type == 'SUBSURF':
|
|
info.update({
|
|
'levels': mod.levels,
|
|
'render_levels': mod.render_levels,
|
|
'subdivision_type': mod.subdivision_type,
|
|
'use_limit_surface': mod.use_limit_surface
|
|
})
|
|
elif mod.type == 'MIRROR':
|
|
info.update({
|
|
'use_axis': [mod.use_axis[0], mod.use_axis[1], mod.use_axis[2]],
|
|
'use_bisect_axis': [mod.use_bisect_axis[0], mod.use_bisect_axis[1], mod.use_bisect_axis[2]],
|
|
'use_bisect_flip_axis': [mod.use_bisect_flip_axis[0], mod.use_bisect_flip_axis[1], mod.use_bisect_flip_axis[2]],
|
|
'mirror_object': mod.mirror_object.name if mod.mirror_object else None,
|
|
'use_clip': mod.use_clip,
|
|
'use_mirror_merge': mod.use_mirror_merge,
|
|
'merge_threshold': mod.merge_threshold
|
|
})
|
|
elif mod.type == 'ARRAY':
|
|
info.update({
|
|
'count': mod.count,
|
|
'use_constant_offset': mod.use_constant_offset,
|
|
'use_relative_offset': mod.use_relative_offset,
|
|
'use_object_offset': mod.use_object_offset,
|
|
'constant_offset_displace': list(mod.constant_offset_displace),
|
|
'relative_offset_displace': list(mod.relative_offset_displace),
|
|
'offset_object': mod.offset_object.name if mod.offset_object else None
|
|
})
|
|
elif mod.type == 'BEVEL':
|
|
info.update({
|
|
'width': mod.width,
|
|
'segments': mod.segments,
|
|
'limit_method': mod.limit_method,
|
|
'offset_type': mod.offset_type,
|
|
'profile': mod.profile,
|
|
'material': mod.material
|
|
})
|
|
elif mod.type == 'BOOLEAN':
|
|
info.update({
|
|
'operation': mod.operation,
|
|
'object': mod.object.name if mod.object else None,
|
|
'solver': mod.solver
|
|
})
|
|
elif mod.type == 'SOLIDIFY':
|
|
info.update({
|
|
'thickness': mod.thickness,
|
|
'offset': mod.offset,
|
|
'use_rim': mod.use_rim,
|
|
'use_even_offset': mod.use_even_offset,
|
|
'material_offset': mod.material_offset
|
|
})
|
|
|
|
return info
|
|
|
|
|
|
def reorder_modifier(object_name: str, modifier_name: str, direction: str) -> Dict[str, Any]:
|
|
"""모디파이어 순서 변경
|
|
|
|
Args:
|
|
object_name: 대상 오브젝트 이름
|
|
modifier_name: 모디파이어 이름
|
|
direction: 'UP' 또는 'DOWN'
|
|
"""
|
|
logger.info(f"Moving modifier '{modifier_name}' {direction} on '{object_name}'")
|
|
|
|
obj = bpy.data.objects.get(object_name)
|
|
if not obj:
|
|
raise ValueError(f"Object '{object_name}' not found")
|
|
|
|
mod = obj.modifiers.get(modifier_name)
|
|
if not mod:
|
|
raise ValueError(f"Modifier '{modifier_name}' not found on '{object_name}'")
|
|
|
|
# 오브젝트 선택 및 활성화
|
|
bpy.context.view_layer.objects.active = obj
|
|
obj.select_set(True)
|
|
|
|
if direction.upper() == 'UP':
|
|
bpy.ops.object.modifier_move_up(modifier=modifier_name)
|
|
elif direction.upper() == 'DOWN':
|
|
bpy.ops.object.modifier_move_down(modifier=modifier_name)
|
|
else:
|
|
raise ValueError(f"Invalid direction '{direction}'. Use 'UP' or 'DOWN'")
|
|
|
|
# 현재 순서 반환
|
|
modifiers = [m.name for m in obj.modifiers]
|
|
|
|
return {
|
|
'status': 'success',
|
|
'modifier': modifier_name,
|
|
'new_order': modifiers
|
|
}
|