from pathlib import Path
from typing import Dict, Literal, Optional, Tuple
from loguru import logger
from ..data_structure.constants import Vector, default_level_blender
from ..material.material_blender import MaterialBlender
from ..object.object_utils import ObjectUtilsBlender
from ..rpc import remote_blender
from ..utils import Validator
from ..utils.functions import blender_functions
from .actor_base import ActorBase
try:
import bpy # isort:skip
from XRFeitoriaBpy.core.factory import XRFeitoriaBlenderFactory # defined in src/XRFeitoriaBpy/core/factory.py
except ModuleNotFoundError:
pass
try:
from ..data_structure.models import TransformKeys # isort:skip
except (ImportError, ModuleNotFoundError):
pass
[docs]
@remote_blender(dec_class=True, suffix='_in_engine')
class ActorBlender(ActorBase):
"""Actor class for Blender."""
_object_utils = ObjectUtilsBlender
[docs]
def set_origin_to_center(self) -> None:
"""Set origin of the object to its center."""
self._object_utils.set_origin(self.name)
def set_material(self, mat: MaterialBlender) -> None:
self._set_material_in_engine(actor_name=self.name, mat_name=mat._name)
#####################################
###### RPC METHODS (Private) ########
#####################################
@staticmethod
def _get_stencil_value_in_engine(actor_name: str) -> int:
"""Get stencil value (pass index) of the actor in Blender.
Args:
actor_name (str): Name of the actor.
Returns:
int: Stencil value (pass index).
"""
return bpy.data.objects[actor_name].pass_index
@staticmethod
def _get_mask_color_in_engine(actor_name: str) -> 'Vector':
"""Get mask color of the actor in Blender.
Args:
actor_name (str): Name of the actor.
Returns:
Vector: Mask color. (r, g, b) in [0, 255].
"""
pass_index = ActorBlender._get_stencil_value_in_engine(actor_name=actor_name)
return (pass_index, pass_index, pass_index)
@staticmethod
def _set_stencil_value_in_engine(actor_name: str, value: int) -> int:
"""Set pass index (stencil value) of the actor in Blender.
Args:
actor_name (str): Name of the actor.
value (int in [0, 255]): Pass index (stencil value).
"""
object = bpy.data.objects[actor_name]
object.pass_index = value
for child in object.children_recursive:
child.pass_index = value
@staticmethod
def _import_actor_from_file_in_engine(file_path: str, actor_name: str, collection_name: str = None) -> None:
"""Import actor from file.
Args:
path (str): File path used of the actor. Support: fbx | obj | alembic.
name (str): Name of the actor in Blender.
collection_name (str, optional): Name of the collection to import the actor to.
Raises:
TypeError: If 'path' is not `fbx | obj | alembic | ply | stl` file.
"""
## get scene and collection
scene, collection = XRFeitoriaBlenderFactory.get_scene_and_collection_for_new_object(collection_name)
# import
with XRFeitoriaBlenderFactory.__judge__(name=actor_name, import_path=file_path, scene=scene):
blender_functions.import_file(file_path=file_path)
@staticmethod
def _import_animation_from_file_in_engine(animation_path: str, actor_name: str, action_name: str = None) -> None:
"""Import an animation file.
Args:
animation_path (PathLike): File path used of the actor. Support: json | blend | fbx.
name (str): Name of the actor to apply this animation.
action_name (str, optional): Name of the action, only need when importing from a blend file.
Defaults to None.
Raises:
TypeError: If 'animation_path' is not `json | blend | fbx` file.
"""
anim_file_ext = Path(animation_path).suffix
if anim_file_ext.lower() == '.json':
XRFeitoriaBlenderFactory.import_mo_json(mo_json_file=animation_path, actor_name=actor_name)
elif anim_file_ext.lower() == '.blend':
XRFeitoriaBlenderFactory.import_mo_blend(
mo_blend_file=animation_path, actor_name=actor_name, action_name=action_name
)
elif anim_file_ext.lower() == '.fbx':
XRFeitoriaBlenderFactory.import_mo_fbx(mo_fbx_file=animation_path, actor_name=actor_name)
else:
raise TypeError(f"Invalid anim file, expected 'json', 'blend', or 'fbx' (got {anim_file_ext[1:]} instead).")
@staticmethod
def _set_material_in_engine(actor_name: str, mat_name: str) -> None:
"""Set material to an actor. If the actor has multiple meshes, set material to
the 1st mesh.
Args:
actor_name (str): Name of the actor.
mat_name (str): Name of the material.
"""
actor = bpy.data.objects[actor_name]
material = XRFeitoriaBlenderFactory.get_material(mat_name=mat_name)
if actor.type == 'ARMATURE':
if len(actor.children) == 0:
raise TypeError(f'Actor {actor_name} has no meshes, thus cannot set material to it.')
mesh = actor.children[0]
elif actor.type == 'MESH':
mesh = actor
if mesh.data.materials:
# assign to 1-st material slot
mesh.data.materials[0] = material
else:
# no existing slot
mesh.data.materials.append(material)
[docs]
@remote_blender(dec_class=True, suffix='_in_engine')
class ShapeBlenderWrapper:
"""Wrapper class for shapes in Blender."""
_object_utils = ObjectUtilsBlender
[docs]
@classmethod
def spawn_cube(
cls,
name: str,
size: float = 1.0,
location: Vector = (0, 0, 0),
rotation: Vector = (0, 0, 0),
scale: Vector = (1, 1, 1),
stencil_value: int = 1,
) -> 'ActorBlender':
"""Spawn a cube in the engine.
Args:
name (str): Name of the new added cube.
size (float in [0, inf], optional): Size of the cube. Defaults to 1.0. (unit: meter)
location (Vector, optional): Location of the cube. Defaults to (0, 0, 0).
rotation (Vector, optional): Rotation of the cube. Defaults to (0, 0, 0).
scale (Vector, optional): Scale of the cube. Defaults to (1, 1, 1).
stencil_value (int in [0, 255], optional): Stencil value of the cube. Defaults to 1.
Ref to :ref:`FAQ-stencil-value` for details.
Returns:
ActorBlender: New added cube.
"""
return cls.spawn(
name=name,
type='cube',
location=location,
rotation=rotation,
scale=scale,
stencil_value=stencil_value,
size=size,
)
[docs]
@classmethod
def spawn_plane(
cls,
name: str,
size: float = 1.0,
location: Vector = (0, 0, 0),
rotation: Vector = (0, 0, 0),
scale: Vector = (1, 1, 1),
stencil_value: int = 1,
) -> 'ActorBlender':
"""Spawn a plane in the engine.
Args:
name (str): Name of the new added plane.
size (float in [0, inf], optional): Size of the plane. Defaults to 1.0. (unit: meter)
location (Vector, optional): Location of the plane. Defaults to (0, 0, 0).
rotation (Vector, optional): Rotation of the plane. Defaults to (0, 0, 0).
scale (Vector, optional): Scale of the plane. Defaults to (1, 1, 1).
stencil_value (int in [0, 255], optional): Stencil value of the plane. Defaults to 1.
Ref to :ref:`FAQ-stencil-value` for details.
Returns:
ActorBlender: New added plane.
"""
return cls.spawn(
name=name,
type='plane',
location=location,
rotation=rotation,
scale=scale,
stencil_value=stencil_value,
size=size,
)
[docs]
@classmethod
def spawn_sphere(
cls,
name: str,
radius: float = 1.0,
segments: int = 32,
ring_count: int = 16,
location: Vector = (0, 0, 0),
rotation: Vector = (0, 0, 0),
scale: Vector = (1, 1, 1),
stencil_value: int = 1,
) -> 'ActorBlender':
"""Spawn a `UV sphere <https://docs.blender.org/manual/en/latest/modeling/meshes/primitives.html#uv-sphere>`_ in the engine.
Args:
name (str): Name of the new added UV sphere.
radius (float in [0, inf], optional): Radius of the UV sphere. Defaults to 1.0. (unit: meter)
segments (int in [3, 100000], optional): Number of vertical segments on the sphere. Defaults to 32.
ring_count (int in [3, 100000], optional): Number of horizontal segmentson the sphere. Defaults to 16.
location (Vector, optional): Location of the UV sphere. Defaults to (0, 0, 0).
rotation (Vector, optional): Rotation of the UV sphere. Defaults to (0, 0, 0).
scale (Vector, optional): Scale of the UV sphere. Defaults to (1, 1, 1).
stencil_value (int in [0, 255], optional): Stencil value of the UV sphere. Defaults to 1.
Ref to :ref:`FAQ-stencil-value` for details.
Returns:
ActorBlender: New added UV sphere.
"""
return cls.spawn(
name=name,
type='sphere',
location=location,
rotation=rotation,
scale=scale,
stencil_value=stencil_value,
segments=segments,
ring_count=ring_count,
radius=radius,
)
[docs]
@classmethod
def spawn_ico_sphere(
cls,
name: str,
radius: float = 1.0,
subdivisions: int = 2,
location: Vector = (0, 0, 0),
rotation: Vector = (0, 0, 0),
scale: Vector = (1, 1, 1),
stencil_value: int = 1,
) -> 'ActorBlender':
"""Spawn an `icosphere <https://docs.blender.org/manual/en/latest/modeling/meshes/primitives.html#icosphere>`_ in the engine.
Args:
name (str): Name of the new added icosphere.
radius (float in [0, inf], optional): Radius of the icosphere. Defaults to 1.0. (unit: meter)
subdivisions (int in [1, 10], optional): Number of times of splitting each triangular face on the sphere into four triangles. Defaults to 2.
location (Vector, optional): Location of the icosphere. Defaults to (0, 0, 0).
rotation (Vector, optional): Rotation of the icosphere. Defaults to (0, 0, 0).
scale (Vector, optional): Scale of the icosphere. Defaults to (1, 1, 1).
stencil_value (int in [0, 255], optional): Stencil value of the icosphere. Defaults to 1.
Ref to :ref:`FAQ-stencil-value` for details.
Returns:
ActorBlender: New added icosphere.
"""
return cls.spawn(
name=name,
type='ico_sphere',
location=location,
rotation=rotation,
scale=scale,
stencil_value=stencil_value,
subdivisions=subdivisions,
radius=radius,
)
[docs]
@classmethod
def spawn_cylinder(
cls,
name: str,
radius: float = 1.0,
depth: float = 2.0,
vertices: int = 32,
location: Vector = (0, 0, 0),
rotation: Vector = (0, 0, 0),
scale: Vector = (1, 1, 1),
stencil_value: int = 1,
) -> 'ActorBlender':
"""Spawn a cylinder in the engine.
Args:
name (str): Name of the new added cylinder.
radius (float in [0, inf], optional): Radius of the cylinder's bases. Defaults to 1.0. (unit: meter)
depth (float in [0, inf], optional): Height of the cylinder. Defaults to 2.0. (unit: meter)
vertices (int in [3, 10000000], optional): Number of vertices on the circumference of the base of the cylinder. Defaults to 32.
location (Vector, optional): Location of the cylinder. Defaults to (0, 0, 0).
rotation (Vector, optional): Rotation of the cylinder. Defaults to (0, 0, 0).
scale (Vector, optional): Scale of the cylinder. Defaults to (1, 1, 1).
stencil_value (int in [0, 255], optional): Stencil value of the cylinder. Defaults to 1.
Ref to :ref:`FAQ-stencil-value` for details.
Returns:
ActorBlender: New added cylinder.
"""
return cls.spawn(
name=name,
type='cylinder',
location=location,
rotation=rotation,
scale=scale,
stencil_value=stencil_value,
vertices=vertices,
radius=radius,
depth=depth,
)
[docs]
@classmethod
def spawn_cone(
cls,
name: str,
radius1: float = 1.0,
radius2: float = 0.0,
depth: float = 2.0,
vertices: int = 32,
location: Vector = (0, 0, 0),
rotation: Vector = (0, 0, 0),
scale: Vector = (1, 1, 1),
stencil_value: int = 1,
) -> 'ActorBlender':
"""Spawn a cone in the engine.
Args:
name (str): Name of the new added cone.
radius1 (float in [0, inf], optional): Radius of the circular base of the cone. Defaults to 1.0. (unit: meter).
radius2 (float in [0, inf], optional): Radius of the tip of the cone. Defaults to 0.0. (unit: meter).
depth (float in [0, inf], optional): Height of the cone. Defaults to 2.0. (unit: meter)
vertices (int in [3, 10000000], optional): Number of vertices on the circumference of the base of the cone. Defaults to 32.
location (Vector, optional): Location of the cone. Defaults to (0, 0, 0).
rotation (Vector, optional): Rotation of the cone. Defaults to (0, 0, 0).
scale (Vector, optional): Scale of the cone. Defaults to (1, 1, 1).
stencil_value (int in [0, 255], optional): Stencil value of the cone. Defaults to 1.
Ref to :ref:`FAQ-stencil-value` for details.
Returns:
ActorBlender: New added cone.
"""
return cls.spawn(
name=name,
type='cone',
location=location,
rotation=rotation,
scale=scale,
stencil_value=stencil_value,
vertices=vertices,
radius1=radius1,
radius2=radius2,
depth=depth,
)
[docs]
@classmethod
def spawn(
cls,
type: Literal['plane', 'cube', 'sphere', 'ico_sphere', 'cylinder', 'cone'],
name: Optional[str] = None,
location: Vector = (0, 0, 0),
rotation: Vector = (0, 0, 0),
scale: Vector = (1, 1, 1),
stencil_value: int = 1,
**kwargs,
) -> 'ActorBlender':
"""Spawn a shape(plane, cube, UV sphere, icosphere, cylinder or cone) in the
scene.
Args:
name (str): Name of the new added shape.
mesh_type (enum in ['plane', 'cube', 'sphere', 'icosphere', 'cylinder', 'cone']): Type of new spawn shape.
location (Vector, optional): Location of the shape. Defaults to (0, 0, 0).
rotation (Vector, optional): Rotation of the shape. Defaults to (0, 0, 0).
scale (Vector, optional): Scale of the shape. Defaults to (1, 1, 1).
stencil_value (int in [0, 255], optional): Stencil value of the shape. Defaults to 1.
Ref to :ref:`FAQ-stencil-value` for details.
**kwargs
- ``plane``: size. Ref to :meth:`ShapeBlenderWrapper.spawn_plane <xrfeitoria.actor.actor_blender.ShapeBlenderWrapper.spawn_plane>` for details.
- ``cube``: size. Ref to :meth:`ShapeBlenderWrapper.spawn_cube <xrfeitoria.actor.actor_blender.ShapeBlenderWrapper.spawn_cube>` for details.
- ``sphere``: radius, segments, ring_count. Ref to :meth:`ShapeBlenderWrapper.spawn_sphere <xrfeitoria.actor.actor_blender.ShapeBlenderWrapper.spawn_sphere>` for details.
- ``icosphere``: radius, subdivisions. Ref to :meth:`ShapeBlenderWrapper.spawn_ico_sphere <xrfeitoria.actor.actor_blender.ShapeBlenderWrapper.spawn_ico_sphere>` for details.
- ``cylinder``: radius, depth, vertices. Ref to :meth:`ShapeBlenderWrapper.spawn_cylinder <xrfeitoria.actor.actor_blender.ShapeBlenderWrapper.spawn_cylinder>` for details.
- ``cone``: radius1, radius2, depth, vertices. Ref to :meth:`ShapeBlenderWrapper.spawn_cone <xrfeitoria.actor.actor_blender.ShapeBlenderWrapper.spawn_cone>` for details.
Returns:
ActorBlender: New added shape.
"""
if name is None:
name = cls._object_utils.generate_obj_name(obj_type=type)
cls._object_utils.validate_new_name(name)
Validator.validate_vector(location, 3)
Validator.validate_vector(rotation, 3)
Validator.validate_vector(scale, 3)
cls._object_utils.validate_new_name(name)
cls._spawn_shape_in_engine(
name=name,
type=type,
**kwargs,
)
mesh = ActorBlender(name=name)
mesh.set_transform(location, rotation, scale)
mesh.stencil_value = stencil_value
logger.info(f'[cyan]Spawned[/cyan] {type} "{name}"')
return mesh
#####################################
###### RPC METHODS (Private) ########
#####################################
@staticmethod
def _spawn_shape_in_engine(
name: str,
type: str,
collection_name: str = None,
size: float = 1.0,
segments: int = 32,
ring_count: int = 16,
radius: float = 1.0,
subdivisions: int = 2,
vertices: int = 32,
depth: float = 2.0,
radius1: float = 0.0,
radius2: float = 2.0,
) -> None:
"""Spawn a shape in Blender.
Args:
name (str): Name of the new added shape.
mesh_type (enum in ['plane', 'cube', 'UV sphere', 'icosphere', 'cylinder', 'cone']): Type of new spawn shape.
size (float in [0, inf], optional): Size. Defaults to 1.0. (unit: meter)
segments (int in [3, 100000], optional): Segments. Defaults to 32.
ring_count (int in [3, 100000], optional): Ring count. Defaults to 16.
radius (float in [0, inf], optional): Radius. Defaults to 1.0. (unit: meter)
subdivisions (int in [1, 10], optional): Subdivisions. Defaults to 2.
vertices (int in [3, 10000000], optional): Vertices. Defaults to 32.
depth (float in [0, inf], optional): Depth. Defaults to 2.0. (unit: meter)
radius1 (float in [0, inf], optional): Radius1. Defaults to 0.0. (unit: meter)
radius2 (float in [0, inf], optional): Radius2. Defaults to 2.0. (unit: meter)
Raises:
TypeError: If `mesh_type` is not in Enum ['plane', 'cube', 'UV sphere', 'icosphere', 'cylinder', 'cone']
"""
## get scene and collection
scene, collection = XRFeitoriaBlenderFactory.get_scene_and_collection_for_new_object(collection_name)
# spawn mesh
with XRFeitoriaBlenderFactory.__judge__(name, scene=scene):
if type == 'plane':
bpy.ops.mesh.primitive_plane_add(size=size)
elif type == 'cube':
bpy.ops.mesh.primitive_cube_add(size=size)
elif type == 'sphere':
bpy.ops.mesh.primitive_uv_sphere_add(
segments=segments,
ring_count=ring_count,
radius=radius,
)
elif type == 'ico_sphere':
bpy.ops.mesh.primitive_ico_sphere_add(
subdivisions=subdivisions,
radius=radius,
)
elif type == 'cylinder':
bpy.ops.mesh.primitive_cylinder_add(
vertices=vertices,
radius=radius,
depth=depth,
)
elif type == 'cone':
bpy.ops.mesh.primitive_cone_add(
vertices=vertices,
radius1=radius1,
radius2=radius2,
depth=depth,
)
else:
raise TypeError(
f'Unsupported mesh type, expected "plane", "cube", "sphere", '
f'"ico_sphere", "cylinder" or "cone", (got "{type}" instead).'
)