Initial commit
This commit is contained in:
552
skills/addon/commands/geometry.py
Normal file
552
skills/addon/commands/geometry.py
Normal file
@@ -0,0 +1,552 @@
|
||||
"""
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user