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