# This file is part of Korman. # # Korman is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Korman is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Korman. If not, see . from __future__ import annotations import bmesh import bpy import mathutils from contextlib import contextmanager from typing import * from PyHSPlasma import * from ..import helpers def affine_parts(xform): # Decompose the matrix into the 90s-era 3ds max affine parts sillyness # All that's missing now is something like "(c) 1998 HeadSpin" oh wait... affine = hsAffineParts() affine.T = hsVector3(*xform.to_translation()) affine.K = hsVector3(*xform.to_scale()) affine.F = -1.0 if xform.determinant() < 0.0 else 1.0 rot = xform.to_quaternion() affine.Q = quaternion(rot) rot.normalize() affine.U = quaternion(rot) return affine def color(blcolor, alpha=1.0): """Converts a Blender Color into an hsColorRGBA""" return hsColorRGBA(blcolor.r, blcolor.g, blcolor.b, alpha) def matrix44(blmat): """Converts a mathutils.Matrix to an hsMatrix44""" hsmat = hsMatrix44() for i in range(4): hsmat[i, 0] = blmat[i][0] hsmat[i, 1] = blmat[i][1] hsmat[i, 2] = blmat[i][2] hsmat[i, 3] = blmat[i][3] return hsmat def quaternion(blquat): """Converts a mathutils.Quaternion to an hsQuat""" return hsQuat(blquat.x, blquat.y, blquat.z, blquat.w) class BMeshObject: def __init__(self, name: str, managed: bool = True): self._managed = managed self._bmesh = None self._mesh = bpy.data.meshes.new(name) self._obj = bpy.data.objects.new(name, self._mesh) self._obj.draw_type = "WIRE" bpy.context.scene.objects.link(self._obj) def __del__(self): if self._managed: bpy.context.scene.objects.unlink(self._obj) bpy.data.meshes.remove(self._mesh) def __enter__(self) -> bmesh.types.BMesh: if self._mesh is not None: self._bmesh = bmesh.new() self._bmesh.from_mesh(self._mesh) return self._bmesh def __exit__(self, type, value, traceback): if self._bmesh is not None: self._bmesh.to_mesh(self._mesh) self._bmesh.free() self._bmesh = None def __getattr__(self, name: str) -> Any: return getattr(self._obj, name) def __setattr__(self, name: str, value: Any) -> None: # NOTE: Calling `hasattr()` will trigger infinite recursion in __getattr__(), so # check the object dict itself for anything that we want on this instance. d = self.__dict__ if name not in d: obj = d.get("_obj") if obj is not None: if hasattr(obj, name): setattr(obj, name, value) return super().__setattr__(name, value) @property def object(self) -> bpy.types.Object: return self._obj def release(self) -> bpy.types.Object: self._managed = False return self._obj def create_empty_object(name: str, owner_object: Optional[bpy.types.Object] = None) -> bpy.types.Object: empty_object = bpy.data.objects.new(name, None) if owner_object is not None: empty_object.plasma_object.enabled = owner_object.plasma_object.enabled empty_object.plasma_object.page = owner_object.plasma_object.page bpy.context.scene.objects.link(empty_object) return empty_object def create_camera_object(name: str) -> bpy.types.Object: cam_data = bpy.data.cameras.new(name) cam_obj = bpy.data.objects.new(name, cam_data) bpy.context.scene.objects.link(cam_obj) return cam_obj def create_cube_region(name: str, size: float, owner_object: bpy.types.Object) -> bpy.types.Object: """Create a cube shaped region object""" region_object = BMeshObject(name) region_object.plasma_object.enabled = True region_object.plasma_object.page = owner_object.plasma_object.page region_object.hide_render = True with region_object as bm: bmesh.ops.create_cube(bm, size=(size)) bmesh.ops.transform( bm, matrix=mathutils.Matrix.Translation( owner_object.matrix_world.translation - region_object.matrix_world.translation ), space=region_object.matrix_world, verts=bm.verts ) return region_object.release() @contextmanager def temporary_camera_object(scene: bpy.types.Scene, name: str) -> bpy.types.Object: try: cam_data = bpy.data.cameras.new(name) cam_obj = bpy.data.objects.new(name, cam_data) scene.objects.link(cam_obj) yield cam_obj finally: cam_obj = locals().get("cam_obj") if cam_obj is not None: bpy.data.objects.remove(cam_obj) cam_data = locals().get("cam_data") if cam_data is not None: bpy.data.cameras.remove(cam_data) @contextmanager def temporary_mesh_object(source : bpy.types.Object) -> bpy.types.Object: """Creates a temporary mesh object from a nonmesh object that will only exist for the duration of the context.""" assert source.type != "MESH" obj = bpy.data.objects.new(source.name, source.to_mesh(bpy.context.scene, True, "RENDER")) obj.draw_type = "WIRE" obj.parent = source.parent obj.matrix_local, obj.matrix_world = source.matrix_local, source.matrix_world bpy.context.scene.objects.link(obj) try: yield obj finally: bpy.data.objects.remove(obj) def transform_mesh(mesh: bpy.types.Mesh, matrix: mathutils.Matrix): # There is a disparity in terms of how negative scaling is displayed in Blender versus how it is # applied (Ctrl+A) in that the normals are different. Even though negative scaling is evil, we # prefer to match the visual behavior, not the non-intuitive apply behavior. So, we'll need to # flip the normals if the scaling is negative. The Blender documentation even "helpfully" warns # us about this. mesh.transform(matrix) if matrix.is_negative: mesh.flip_normals()