553 lines
13 KiB
Python
553 lines
13 KiB
Python
"""
|
|
Geometry Operations
|
|
도형 생성, 수정, 삭제 등 기하학적 작업을 처리하는 명령 핸들러
|
|
"""
|
|
|
|
import bpy
|
|
import bmesh
|
|
from typing import Dict, List, Tuple, Optional, Any
|
|
from ..utils.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# Primitive Creation (기본 도형 생성)
|
|
# ============================================================================
|
|
|
|
def create_cube(
|
|
location: Tuple[float, float, float] = (0, 0, 0),
|
|
size: float = 2.0,
|
|
name: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
큐브 생성
|
|
|
|
Args:
|
|
location: 위치 (x, y, z)
|
|
size: 크기
|
|
name: 오브젝트 이름 (None이면 자동 생성)
|
|
|
|
Returns:
|
|
생성된 오브젝트 정보
|
|
"""
|
|
logger.info(f"Creating cube at {location} with size {size}")
|
|
|
|
bpy.ops.mesh.primitive_cube_add(size=size, location=location)
|
|
obj = bpy.context.active_object
|
|
|
|
if name:
|
|
obj.name = name
|
|
|
|
return {
|
|
'name': obj.name,
|
|
'type': obj.type,
|
|
'location': list(obj.location),
|
|
'vertices': len(obj.data.vertices),
|
|
'faces': len(obj.data.polygons)
|
|
}
|
|
|
|
|
|
def create_sphere(
|
|
location: Tuple[float, float, float] = (0, 0, 0),
|
|
radius: float = 1.0,
|
|
segments: int = 32,
|
|
ring_count: int = 16,
|
|
name: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
구(Sphere) 생성
|
|
|
|
Args:
|
|
location: 위치 (x, y, z)
|
|
radius: 반지름
|
|
segments: 세그먼트 수 (수평)
|
|
ring_count: 링 수 (수직)
|
|
name: 오브젝트 이름
|
|
|
|
Returns:
|
|
생성된 오브젝트 정보
|
|
"""
|
|
logger.info(f"Creating sphere at {location} with radius {radius}")
|
|
|
|
bpy.ops.mesh.primitive_uv_sphere_add(
|
|
radius=radius,
|
|
segments=segments,
|
|
ring_count=ring_count,
|
|
location=location
|
|
)
|
|
obj = bpy.context.active_object
|
|
|
|
if name:
|
|
obj.name = name
|
|
|
|
return {
|
|
'name': obj.name,
|
|
'type': obj.type,
|
|
'location': list(obj.location),
|
|
'vertices': len(obj.data.vertices),
|
|
'faces': len(obj.data.polygons)
|
|
}
|
|
|
|
|
|
def create_cylinder(
|
|
location: Tuple[float, float, float] = (0, 0, 0),
|
|
radius: float = 1.0,
|
|
depth: float = 2.0,
|
|
vertices: int = 32,
|
|
name: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
실린더 생성
|
|
|
|
Args:
|
|
location: 위치 (x, y, z)
|
|
radius: 반지름
|
|
depth: 높이
|
|
vertices: 버텍스 수
|
|
name: 오브젝트 이름
|
|
|
|
Returns:
|
|
생성된 오브젝트 정보
|
|
"""
|
|
logger.info(f"Creating cylinder at {location}")
|
|
|
|
bpy.ops.mesh.primitive_cylinder_add(
|
|
radius=radius,
|
|
depth=depth,
|
|
vertices=vertices,
|
|
location=location
|
|
)
|
|
obj = bpy.context.active_object
|
|
|
|
if name:
|
|
obj.name = name
|
|
|
|
return {
|
|
'name': obj.name,
|
|
'type': obj.type,
|
|
'location': list(obj.location),
|
|
'vertices': len(obj.data.vertices),
|
|
'faces': len(obj.data.polygons)
|
|
}
|
|
|
|
|
|
def create_plane(
|
|
location: Tuple[float, float, float] = (0, 0, 0),
|
|
size: float = 2.0,
|
|
name: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
평면(Plane) 생성
|
|
|
|
Args:
|
|
location: 위치 (x, y, z)
|
|
size: 크기
|
|
name: 오브젝트 이름
|
|
|
|
Returns:
|
|
생성된 오브젝트 정보
|
|
"""
|
|
logger.info(f"Creating plane at {location}")
|
|
|
|
bpy.ops.mesh.primitive_plane_add(size=size, location=location)
|
|
obj = bpy.context.active_object
|
|
|
|
if name:
|
|
obj.name = name
|
|
|
|
return {
|
|
'name': obj.name,
|
|
'type': obj.type,
|
|
'location': list(obj.location),
|
|
'vertices': len(obj.data.vertices),
|
|
'faces': len(obj.data.polygons)
|
|
}
|
|
|
|
|
|
def create_cone(
|
|
location: Tuple[float, float, float] = (0, 0, 0),
|
|
radius1: float = 1.0,
|
|
depth: float = 2.0,
|
|
vertices: int = 32,
|
|
name: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
원뿔(Cone) 생성
|
|
|
|
Args:
|
|
location: 위치 (x, y, z)
|
|
radius1: 아래 반지름
|
|
depth: 높이
|
|
vertices: 버텍스 수
|
|
name: 오브젝트 이름
|
|
|
|
Returns:
|
|
생성된 오브젝트 정보
|
|
"""
|
|
logger.info(f"Creating cone at {location}")
|
|
|
|
bpy.ops.mesh.primitive_cone_add(
|
|
radius1=radius1,
|
|
depth=depth,
|
|
vertices=vertices,
|
|
location=location
|
|
)
|
|
obj = bpy.context.active_object
|
|
|
|
if name:
|
|
obj.name = name
|
|
|
|
return {
|
|
'name': obj.name,
|
|
'type': obj.type,
|
|
'location': list(obj.location),
|
|
'vertices': len(obj.data.vertices),
|
|
'faces': len(obj.data.polygons)
|
|
}
|
|
|
|
|
|
def create_torus(
|
|
location: Tuple[float, float, float] = (0, 0, 0),
|
|
major_radius: float = 1.0,
|
|
minor_radius: float = 0.25,
|
|
major_segments: int = 48,
|
|
minor_segments: int = 12,
|
|
name: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
토러스(Torus) 생성
|
|
|
|
Args:
|
|
location: 위치 (x, y, z)
|
|
major_radius: 주 반지름
|
|
minor_radius: 부 반지름
|
|
major_segments: 주 세그먼트 수
|
|
minor_segments: 부 세그먼트 수
|
|
name: 오브젝트 이름
|
|
|
|
Returns:
|
|
생성된 오브젝트 정보
|
|
"""
|
|
logger.info(f"Creating torus at {location}")
|
|
|
|
bpy.ops.mesh.primitive_torus_add(
|
|
major_radius=major_radius,
|
|
minor_radius=minor_radius,
|
|
major_segments=major_segments,
|
|
minor_segments=minor_segments,
|
|
location=location
|
|
)
|
|
obj = bpy.context.active_object
|
|
|
|
if name:
|
|
obj.name = name
|
|
|
|
return {
|
|
'name': obj.name,
|
|
'type': obj.type,
|
|
'location': list(obj.location),
|
|
'vertices': len(obj.data.vertices),
|
|
'faces': len(obj.data.polygons)
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# Object Operations (오브젝트 작업)
|
|
# ============================================================================
|
|
|
|
def delete_object(name: str) -> Dict[str, str]:
|
|
"""
|
|
오브젝트 삭제
|
|
|
|
Args:
|
|
name: 오브젝트 이름
|
|
|
|
Returns:
|
|
삭제 결과
|
|
"""
|
|
logger.info(f"Deleting object: {name}")
|
|
|
|
obj = bpy.data.objects.get(name)
|
|
if not obj:
|
|
raise ValueError(f"Object '{name}' not found")
|
|
|
|
bpy.data.objects.remove(obj, do_unlink=True)
|
|
|
|
return {'status': 'success', 'message': f"Object '{name}' deleted"}
|
|
|
|
|
|
def transform_object(
|
|
name: str,
|
|
location: Optional[Tuple[float, float, float]] = None,
|
|
rotation: Optional[Tuple[float, float, float]] = None,
|
|
scale: Optional[Tuple[float, float, float]] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
오브젝트 변형 (이동, 회전, 스케일)
|
|
|
|
Args:
|
|
name: 오브젝트 이름
|
|
location: 위치 (x, y, z)
|
|
rotation: 회전 (x, y, z) in radians
|
|
scale: 스케일 (x, y, z)
|
|
|
|
Returns:
|
|
변형된 오브젝트 정보
|
|
"""
|
|
logger.info(f"Transforming object: {name}")
|
|
|
|
obj = bpy.data.objects.get(name)
|
|
if not obj:
|
|
raise ValueError(f"Object '{name}' not found")
|
|
|
|
if location:
|
|
obj.location = location
|
|
|
|
if rotation:
|
|
obj.rotation_euler = rotation
|
|
|
|
if scale:
|
|
obj.scale = scale
|
|
|
|
return {
|
|
'name': obj.name,
|
|
'location': list(obj.location),
|
|
'rotation': list(obj.rotation_euler),
|
|
'scale': list(obj.scale)
|
|
}
|
|
|
|
|
|
def duplicate_object(
|
|
name: str,
|
|
new_name: Optional[str] = None,
|
|
location: Optional[Tuple[float, float, float]] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
오브젝트 복제
|
|
|
|
Args:
|
|
name: 원본 오브젝트 이름
|
|
new_name: 새 오브젝트 이름 (None이면 자동 생성)
|
|
location: 새 위치 (None이면 원본 위치)
|
|
|
|
Returns:
|
|
복제된 오브젝트 정보
|
|
"""
|
|
logger.info(f"Duplicating object: {name}")
|
|
|
|
obj = bpy.data.objects.get(name)
|
|
if not obj:
|
|
raise ValueError(f"Object '{name}' not found")
|
|
|
|
# 복제
|
|
new_obj = obj.copy()
|
|
new_obj.data = obj.data.copy()
|
|
|
|
if new_name:
|
|
new_obj.name = new_name
|
|
|
|
if location:
|
|
new_obj.location = location
|
|
|
|
# 씬에 추가
|
|
bpy.context.collection.objects.link(new_obj)
|
|
|
|
return {
|
|
'name': new_obj.name,
|
|
'type': new_obj.type,
|
|
'location': list(new_obj.location)
|
|
}
|
|
|
|
|
|
def list_objects(object_type: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
"""
|
|
씬의 오브젝트 목록 조회
|
|
|
|
Args:
|
|
object_type: 오브젝트 타입 필터 (None이면 전체)
|
|
예: 'MESH', 'ARMATURE', 'CAMERA', 'LIGHT'
|
|
|
|
Returns:
|
|
오브젝트 목록
|
|
"""
|
|
logger.info(f"Listing objects (type: {object_type or 'ALL'})")
|
|
|
|
objects = []
|
|
for obj in bpy.data.objects:
|
|
if object_type and obj.type != object_type:
|
|
continue
|
|
|
|
objects.append({
|
|
'name': obj.name,
|
|
'type': obj.type,
|
|
'location': list(obj.location),
|
|
'rotation': list(obj.rotation_euler),
|
|
'scale': list(obj.scale)
|
|
})
|
|
|
|
return objects
|
|
|
|
|
|
# ============================================================================
|
|
# Vertex Operations (버텍스 작업)
|
|
# ============================================================================
|
|
|
|
def get_vertices(name: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
오브젝트의 버텍스 정보 조회
|
|
|
|
Args:
|
|
name: 오브젝트 이름
|
|
|
|
Returns:
|
|
버텍스 목록
|
|
"""
|
|
logger.info(f"Getting vertices for object: {name}")
|
|
|
|
obj = bpy.data.objects.get(name)
|
|
if not obj or obj.type != 'MESH':
|
|
raise ValueError(f"Mesh object '{name}' not found")
|
|
|
|
vertices = []
|
|
for i, vert in enumerate(obj.data.vertices):
|
|
vertices.append({
|
|
'index': i,
|
|
'co': list(vert.co),
|
|
'normal': list(vert.normal)
|
|
})
|
|
|
|
return vertices
|
|
|
|
|
|
def move_vertex(
|
|
object_name: str,
|
|
vertex_index: int,
|
|
new_position: Tuple[float, float, float]
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
버텍스 이동
|
|
|
|
Args:
|
|
object_name: 오브젝트 이름
|
|
vertex_index: 버텍스 인덱스
|
|
new_position: 새 위치 (x, y, z)
|
|
|
|
Returns:
|
|
수정된 버텍스 정보
|
|
"""
|
|
logger.info(f"Moving vertex {vertex_index} in object {object_name}")
|
|
|
|
obj = bpy.data.objects.get(object_name)
|
|
if not obj or obj.type != 'MESH':
|
|
raise ValueError(f"Mesh object '{object_name}' not found")
|
|
|
|
mesh = obj.data
|
|
if vertex_index >= len(mesh.vertices):
|
|
raise ValueError(f"Vertex index {vertex_index} out of range")
|
|
|
|
mesh.vertices[vertex_index].co = new_position
|
|
mesh.update()
|
|
|
|
return {
|
|
'object': object_name,
|
|
'vertex_index': vertex_index,
|
|
'position': list(mesh.vertices[vertex_index].co)
|
|
}
|
|
|
|
|
|
def subdivide_mesh(
|
|
name: str,
|
|
cuts: int = 1
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
메쉬 세분화 (Subdivide)
|
|
|
|
Args:
|
|
name: 오브젝트 이름
|
|
cuts: 세분화 횟수
|
|
|
|
Returns:
|
|
세분화된 메쉬 정보
|
|
"""
|
|
logger.info(f"Subdividing mesh: {name} (cuts: {cuts})")
|
|
|
|
obj = bpy.data.objects.get(name)
|
|
if not obj or obj.type != 'MESH':
|
|
raise ValueError(f"Mesh object '{name}' not found")
|
|
|
|
# Edit 모드로 전환
|
|
bpy.context.view_layer.objects.active = obj
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
|
|
# 모든 에지 선택
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|
|
|
# 세분화
|
|
bpy.ops.mesh.subdivide(number_cuts=cuts)
|
|
|
|
# Object 모드로 복귀
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
return {
|
|
'name': obj.name,
|
|
'vertices': len(obj.data.vertices),
|
|
'edges': len(obj.data.edges),
|
|
'faces': len(obj.data.polygons)
|
|
}
|
|
|
|
|
|
def extrude_face(
|
|
object_name: str,
|
|
face_index: int,
|
|
offset: float = 1.0
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
페이스 돌출 (Extrude)
|
|
|
|
Args:
|
|
object_name: 오브젝트 이름
|
|
face_index: 페이스 인덱스
|
|
offset: 돌출 거리
|
|
|
|
Returns:
|
|
돌출 결과 정보
|
|
"""
|
|
logger.info(f"Extruding face {face_index} in object {object_name}")
|
|
|
|
obj = bpy.data.objects.get(object_name)
|
|
if not obj or obj.type != 'MESH':
|
|
raise ValueError(f"Mesh object '{object_name}' not found")
|
|
|
|
# BMesh를 사용한 extrude
|
|
mesh = obj.data
|
|
bm = bmesh.new()
|
|
bm.from_mesh(mesh)
|
|
|
|
# 페이스 선택
|
|
if face_index >= len(bm.faces):
|
|
bm.free()
|
|
raise ValueError(f"Face index {face_index} out of range")
|
|
|
|
face = bm.faces[face_index]
|
|
|
|
# Extrude
|
|
ret = bmesh.ops.extrude_face_region(bm, geom=[face])
|
|
extruded_verts = [v for v in ret['geom'] if isinstance(v, bmesh.types.BMVert)]
|
|
|
|
# 오프셋 적용
|
|
for v in extruded_verts:
|
|
v.co += face.normal * offset
|
|
|
|
# 메쉬 업데이트
|
|
bm.to_mesh(mesh)
|
|
bm.free()
|
|
mesh.update()
|
|
|
|
return {
|
|
'object': object_name,
|
|
'face_index': face_index,
|
|
'vertices': len(mesh.vertices),
|
|
'faces': len(mesh.polygons)
|
|
}
|