Source code for ddg.conversion.blender.halfedge

import warnings

import bmesh
import bpy
import deprecation
import numpy as np

import ddg
import ddg.datastructures.halfedge.get as get
import ddg.datastructures.halfedge.set as setutils
import ddg.math.euclidean
from ddg.datastructures.halfedge.surface_generator import cylinder, icosphere
from ddg.visualization.blender.mesh import (
    duplicate_by_transformation_matrices,
    from_bmesh,
    shade_smooth,
)
from ddg.visualization.blender.object import empty


[docs]def hes_to_bmesh(hes, co_attr="co", bpy_data=None): """ Converts a given half-edge surface into a bmesh. The returned bmesh can then be used for handing over the surface to Blender. Parameters ---------- hes : ddg.halfedge.Surface The half-edge surface that shall be converted to a bmesh. co_attr : coordinate attribute The type of coordinates that will be taken for conversion. bpy_data : bmesh (optional, default=None) When given, bmesh to save the data into. Returns ------- bmesh The bmesh corresponding to the input half-edge surface. """ if bpy_data is None: hes_bmesh = bmesh.new() else: hes_bmesh = bpy_data hes_bmesh.clear() co = getattr(hes.verts, co_attr) # create vertices # by adding new vertices to the bmesh for _ in range(len(hes.verts)): hes_bmesh.verts.new() # and handing over the coordinates hes_bmesh.verts.ensure_lookup_table() tmp_dict = dict() # for rebuilding the faces: save the correspondence of # the bmesh-vertex and its corresponding hes-vertex-id for i, v in enumerate(hes.verts): hes_bmesh.verts[i].co = co[v] tmp_dict[id(v)] = hes_bmesh.verts[i] # create faces for f in hes.faces: face_verts = list(get.get_vertices(f)) hes_bmesh.faces.new([tmp_dict[id(v)] for v in face_verts]) # catch edges not belonging to a face for e in get.single_edges(hes): if e.face is None and e.opp.face is None: hes_bmesh.edges.new([tmp_dict[id(e.opp.head)], tmp_dict[id(e.head)]]) return hes_bmesh
[docs]def vertex_objects( heobj, bobj, collection=None, location_attr="co", rotation_attr=None, scale_attr=None, material_attr=None, ): """ places a Blender object at every vertex of the given half-edge object. Parameters ---------- heobj : ddg.halfedge.Surface Half-edge object of which each vertex will have a given Blender object placed on it. bobj : bpy.types.Object Blender object to duplicate. collection : bpy.types.Collection , (default=None) Links all duplicate objects to given collection. if None, then duplicates are not linked. location_attr : str (default='co') Name of vertex attribute storing the Euclidean coordinates. rotation_attr : str (default=None) Name of vertex attribute storing a 4x4 rotation matrix to be applied to the duplicate. scale_attr : str (default=None) Name of vertex attribute storing a 4x4 scaling matrix to be applied to the duplicate. material_attr : str (default=None) Name of vertex attribute storing name of Blender material or a Blender material to be applied to the duplicate. Returns ------- vobjs: list List of Blender objects placed at each vertex of given half-edge object. """ verticies = heobj.verts transformations = [] material = [] for v in verticies: co_obj = np.array(list(getattr(v, location_attr)) + [1]) translation_mat = np.concatenate([np.eye(4, 3), co_obj.reshape(-1, 1)], axis=1) if rotation_attr is not None: rot_obj = getattr(v, rotation_attr) scale_obj = getattr(e, scale_attr) translation_mat = translation_mat @ rot_obj @ scale_obj transformations.append(translation_mat) if material_attr is not None: material.append(getattr(v, material_attr)) if len(material) == 0: material = None vobjs = duplicate_by_transformation_matrices( bobj, transformations, collection, material=material ) return vobjs
[docs]def edge_objects( heobj, bobj, collection=None, location_attr="co", rotation_attr=None, scale_attr=None, material_attr=None, ): """ Places a Blender object at every edge of the given half-edge object. Parameters ---------- see vertex_object. Returns ------- vobjs: list list of Blender objects placed at each edge of given half-edge object. Notes ----- Half-edge edge's does not natively store a location attribute this must be calculated and added as an attribute before hand. """ transformations = [] material = [] for e in get.single_edges(heobj): co_obj = np.array(list(getattr(e, location_attr)) + [1]) translation_mat = np.concatenate([np.eye(4, 3), co_obj.reshape(-1, 1)], axis=1) if rotation_attr is not None: rot_obj = getattr(e, rotation_attr) scale_obj = getattr(e, scale_attr) translation_mat = translation_mat @ rot_obj @ scale_obj transformations.append(translation_mat) if material_attr is not None: material.append(getattr(e, material_attr)) if len(material) == 0: material = None vobjs = duplicate_by_transformation_matrices( bobj, transformations, collection, material ) return vobjs
[docs]def face_objects( heobj, bobj, collection=None, location_attr="co", rotation_attr=None, scale_attr=None, material_attr=None, ): """ places a Blender object at every face of the given half-edge object. Parameters ---------- see vertex_object. Returns ------- vobjs: list list of Blender objects placed at each face of given half-edge object. Notes ----- Half-edge face's does not natively store a location attribute this must be calculated and added as an attribute before hand. """ faces = heobj.faces transformations = [] material = [] for f in faces: co_obj = np.array(list(getattr(f, location_attr)) + [1]) translation_mat = np.concatenate([np.eye(4, 3), co_obj.reshape(-1, 1)], axis=1) if rotation_attr is not None: rot_obj = getattr(f, rotation_attr) scale_obj = getattr(f, scale_attr) translation_mat = translation_mat @ rot_obj @ scale_obj transformations.append(translation_mat) if material_attr is not None: material.append(getattr(f, material_attr)) if len(material) == 0: material = None vobjs = duplicate_by_transformation_matrices( bobj, transformations, collection, material ) return vobjs
[docs]def vertex_spheres( heobj, location_attr="co", sphere_radius=0.1, sphere_subdivision_steps=2, material_attr=None, scale_attr=None, collection=None, ): """ Places a Blender object sphere at every vertex of a given half-edge object. Parameters ---------- heobj : ddg.halfedge.Surface Helfedge object of which each vertex will have a sphere placed location_attr : str (default='co') name of vertex attribute storing the euclidean coordinates. sphere_radius : float (default =0.1) Radius of the icospheres at the vertices. sphere_subdivision_steps : int (default=2) Number of subdivisions of the icospheres at the vertices. material : str (default=None) Name of vertex attribute storing name of Blender material or a Blender material to applied to the vertex sphere. scale_attr : str (default=None) Name of vertex attribute storing a 4x4 scaling matrix to be applied to the vertex sphere. collection : bpy.types.Collection (default=None) links all duplicate objects to given collection Returns ------- List of Blender spheres at each vertex """ sphere_bobj = ddg.to_blender_object_helper( icosphere(subdivision_steps=sphere_subdivision_steps, radius=sphere_radius) ) return vertex_objects( heobj, sphere_bobj, collection=collection, material_attr=material_attr, scale_attr=scale_attr, location_attr=location_attr, )
[docs]def edge_tubes( heobj, tube_radius=0.05, tube_resolution=20, fill_tube_caps=True, material_attr=None, location_attr="co", scale_attr=None, collection=None, atol=None, rtol=None, ): """ Places a Blender object cylinder at every edge of a given half-edge object. Parameters ---------- heobj : ddg.halfedge.Surface Half-edge object of which each edge will have a cylinder placed. tube_radius : float (default=0.5) Radius of the edge tube. tube_resolution : int (default=20) Resolution of the edge tube, i.e., number of faces of a tube. fill_tube_caps : bool (default=True) Decides whether the ends of the tubes are closed by faces (or left open). material : str (default=None) Name of vertex attribute storing name of Blender material or a Blender material to applied to the edge tube. scale_attr : str (default=None) Name of vertex attribute storing a 4x4 scaling matrix to be applied to the edge tube. location_attr : str (default='co') name of vertex attribute storing the euclidean coordinates. collection : bpy.types.Collection (default=None) links all duplicate objects to given collection. atol, rtol : float (default=None) If None, the global defaults will be used. See :py:mod:`ddg.nonexact` for details. Returns ------- List of Blender cylinders placed along each edge. """ tube_bobj = ddg.to_blender_object_helper( cylinder( bot_radius=tube_radius, top_radius=tube_radius, resolution=tube_resolution, fill_caps=fill_tube_caps, ) ) heobj.edges.add_attribute("matrix_world") heobj.edges.add_attribute("location") for e in get.single_edges(heobj): location = np.array(getattr(e.tail, location_attr)) matrix_world = ddg.math.euclidean.scale_rotation_from_to( (0, 1, 0), getattr(e.head, location_attr) - getattr(e.tail, location_attr), True, atol=atol, rtol=rtol, ) e.matrix_world = matrix_world e.location = location return edge_objects( heobj, tube_bobj, collection=collection, location_attr="location", rotation_attr="matrix_world", material_attr=material_attr, scale_attr=scale_attr, )
[docs]@deprecation.deprecated( deprecated_in="0.1.0", removed_in="0.2.0", current_version=ddg.__version__, details="Use `vertex_spheres` and `edge_tubes` instead.", ) def hes_to_tubes_and_spheres_blender_object( hes, co_attr="co", tube_resolution=20, fill_tube_caps=True, tube_radius=0.05, sphere_subdivision_steps=2, sphere_radius=0.1, parent_kwargs={}, kwargs_generator=None, atol=None, rtol=None, ): """ Converts a given half-edge surface to a Blender object consisting of tubes and spheres. The tubes represent the edges and the spheres represent the vertices of the surface. Their respective Blender objects are linked to a common parent object. Smooth shading is always enabled. Parameters ---------- hes : ddg.halfedge.Surface The half-edge surface that shall be converted to a Blender object. co_attr : str (default='co') The name of vertex attribute storing the euclidean coordinates. tube_resolution: int (default=20) Resolution of the edge tube, i.e., number of faces of a tube. fill_tube_caps: bool (default=False) Decides whether the ends of the tubes are closed by faces (or left open). tube_radius: float (default=.05) Radius of the edge tube. sphere_subdivision_steps: int (default=2) Number of subdivisions of the icospheres at the vertices. sphere_radius: float (default=.1) Radius of the icospheres at the vertices. parent_kwargs: dict (default={}) Dictionary containing the keyword arguments that are handed to ddg.to_blender_object_helper when initializing the parent Blender object. kwargs_generator: function (default=None) Function of signature kwargs_generator(cell) returning a kwargs dictionary where a cell can either be a vertex or an edge. If such a function is given, the returned dictionary will be handed over to ddg.to_blender_object_helper when creating the tubes and spheres. For example, it simply could return different material names for vertex spheres and edge tubes, respectively. atol, rtol : float (default=None) If None, the global defaults will be used. See :py:mod:`ddg.nonexact` for details. Returns ------- blender.types.Object Blender object that stores tubes and spheres as children. """ setutils.set_euclidean_length_attr(hes, co_attr=co_attr, attr_name="length") # create parent Blender object parent_bobj = empty(**parent_kwargs) # create hds sphere and tube sphere = icosphere(subdivision_steps=sphere_subdivision_steps, radius=sphere_radius) tube = cylinder(resolution=tube_resolution, fill_caps=fill_tube_caps) for v in hes.verts: sphere_kwargs = {} if kwargs_generator is None else kwargs_generator(v) location = np.array( sphere_kwargs.get("attributes", {}).get("location", np.array([0, 0, 0])) ) + np.array(getattr(v, co_attr)) # update attributes by new parent sphere_kwargs["attributes"] = sphere_kwargs.get("attributes", {}) if sphere_kwargs["attributes"].get("parent", None) is not None: warnings.warn( 'Given attributes for vertex sphere contain "parent" that will be' " overwritten!" ) sphere_kwargs["attributes"].update( {"location": location, "parent": parent_bobj} ) # update kwargs by shade_smooth mesh transformation sphere_kwargs["mesh_transformations"] = sphere_kwargs.get( "mesh_transformations", [] ) sphere_kwargs["mesh_transformations"] = ( sphere_kwargs["mesh_transformations"] if type(sphere_kwargs["mesh_transformations"]) is list else [sphere_kwargs["mesh_transformations"]] ) sphere_kwargs["mesh_transformations"].extend([shade_smooth]) ddg.to_blender_object_helper(sphere, **sphere_kwargs) for e in get.single_edges(hes): matrix_world = ddg.math.euclidean.rotation_from_to( getattr(e.head, co_attr) - getattr(e.tail, co_attr), (0, 1, 0), True, atol=atol, rtol=rtol, ) tube_kwargs = {} if kwargs_generator is None else kwargs_generator(e) # update attributes by matrix_world, parent and scale attributes = tube_kwargs.get("attributes", {}) location = attributes.get("location", np.array([0, 0, 0])) + np.array( getattr(e.tail, co_attr) ) if attributes.get("location"): del attributes["location"] if np.any( [ attributes.get("matrix_world", None), attributes.get("parent", None), attributes.get("scale", None), ] ): warnings.warn( 'Given attributes for edge tube contain either "matrix_world",' ' "parent", or "scale" that will be overwritten!' ) attributes.update( { "matrix_world": matrix_world, "location": location, "parent": parent_bobj, "scale": (tube_radius, e.length, tube_radius), } ) tube_kwargs["attributes"] = attributes # update kwargs by shade_smooth mesh transformation tube_kwargs["mesh_transformations"] = tube_kwargs.get( "mesh_transformations", [] ) tube_kwargs["mesh_transformations"] = ( tube_kwargs["mesh_transformations"] if type(tube_kwargs["mesh_transformations"]) is list else [tube_kwargs["mesh_transformations"]] ) tube_kwargs["mesh_transformations"].extend([shade_smooth]) ddg.to_blender_object_helper(tube, **tube_kwargs) delattr(hes.edges, "length") return parent_bobj
[docs]def hes_to_mesh(hes, name="Halfedge mesh", co_attr="co"): hes_bmesh = hes_to_bmesh(hes, co_attr=co_attr) return from_bmesh(hes_bmesh, name, free=True)
[docs]def hes_to_blender_object( hes, name="Halfedge object", co_attr="co", parent=None, matrix=np.eye(4) ): hes_mesh = hes_to_mesh(hes, name=name, co_attr=co_attr) obj = bpy.data.objects.new(name, hes_mesh) obj.parent = parent obj.matrix_local = matrix bpy.context.scene.collection.objects.link(obj) return obj