Source code for ddg.blender._conversion

from random import random

import bpy
import numpy as np

import ddg
import ddg._error_messages as em
from ddg.arrays import (
    Curve,
    CurveList,
    Mesh,
    Points,
    _default_curve_radius,
    _default_curve_sampling,
    _default_icosphere_subdivision_steps,
    _default_point_mesh,
    _default_point_radius,
    _default_subspace_size,
    _default_surface_sampling,
    _points_to_mesh,
)
from ddg.blender.object import delete


[docs]def convert( convertible, name, /, material=None, collection=None, bounding_box=(np.inf, np.inf, np.inf), curve_sampling=_default_curve_sampling, surface_sampling=_default_surface_sampling, subspace_size=_default_subspace_size, curve_radius=_default_curve_radius, point_radius=_default_point_radius, icosphere_subdivision_steps=_default_icosphere_subdivision_steps, ): """Convert pyddg objects, points, curves and meshes to Blender objects. Parameters ---------- convertible This must be an instance of:: - `ddg.geometry.Subspace` of dimension <= 2 - `ddg.geometry.Quadric` of dimension <= 2 - `ddg.geometry.QuadricIntersection` of dimension_complex == 1 - `ddg.geometry.spheres.QuadricSphere` of dimension <= 2 - `ddg.indexedfaceset.IndexedFaceSet` with coordinate attribute "co" - `ddg.halfedge.Surface` with coordinate attribute "co" - `ddg.nets.SmoothCurve` - `ddg.nets.SmoothNet` - `ddg.nets.DiscreteCurve` - `ddg.nets.DiscreteNet` - `ddg.nets.PointNet` - `ddg.nets.EmptyNet` - `ddg.nets.NetCollection` of smooth or discrete nets of the same dimension - `ddg.arrays.Points` - `ddg.arrays.Curve` - `ddg.arrays.CurveList` - `ddg.arrays.Mesh` name : str Sets `bobj.name` and `bobj.data.name` to this value. material : str or bpy.types.Material (default=None) The resulting Blender object is assigned this material if it isn't `None`. collection : bpy.types.Collection (default=None) The resulting Blender object is linked to this collection if it isn't `None`. bounding_box : array_like of shape (3,) of type float (default=(np.inf, np.inf, np.inf)) Bounding box of the resulting blender object. curve_sampling : tuple of an int and a float (default=_default_curve_sampling) Determines the sample number and stepsize for curves surface_sampling : tuple of an int and a float (default=_default_surface_sampling) Determines the sample number and stepsize for surfaces subspace_size : float (default=_default_subspace_size) Determines the size of subspaces curve_radius : float (default=_default_curve_radius) Radius for curves, set as `bobj.data.bevel_depth` point_radius : float (default=_default_point_radius) Radius of the sphere representing points icosphere_subdivision_steps : int (default=_default_icosphere_subdivision_steps) Number of subdivision steps for the icosphere representing points Returns ------- bpy.types.Object The `data` attribute is of type `bpy.types.Curve` if `convertible` represents a curve curve and `bpy.types.Mesh` represents a point, points or a mesh. Examples -------- >>> import bpy >>> import numpy as np >>> import ddg To display a 2-dimensional subspace >>> plane = ddg.geometry.subspace_from_affine_points( ... (0, 0, 0), (1, 0, 0), (0, 1, 0) ... ) >>> ddg.blender.convert(plane, "plane") bpy.data.objects['plane'] To display a quadric >>> two_sheeted_hyperboloid = ddg.geometry.Quadric(np.diag([1, 1, -1, 1])) >>> ddg.blender.convert(two_sheeted_hyperboloid, "two sheeted hyperboloid") bpy.data.objects['two sheeted hyperboloid'] To display a curve represented by a discrete curve >>> discrete_curve = ddg.nets.DiscreteCurve( ... lambda t: (np.cos(t), np.sin(t), t), (-10, 10) ... ) >>> ddg.blender.convert(discrete_curve, "discrete_curve") bpy.data.objects['discrete_curve'] To display a surface represented by a discrete net >>> discrete_net = ddg.nets.DiscreteNet( ... lambda u, v: (u, v, np.cos(v)), ((-10, 10), (-10, 10)) ... ) >>> ddg.blender.convert(discrete_net, "discrete_net") bpy.data.objects['discrete_net'] To create a curve object manually >>> curve = ddg.arrays.Curve([(0, 0, 0), (1, 0, 0)], False) >>> curve_object = ddg.blender.convert(curve, "Curve") >>> type(curve_object.data) <class 'bpy.types.Curve'> >>> curve_object.data.name 'Curve' >>> curve_object.name 'Curve' To create a mesh object manually >>> mesh = ddg.arrays.Mesh([(0, 0, 0), (1, 0, 0), (0, 1, 0)], [(0, 1, 2)]) >>> mesh_object = ddg.blender.convert(mesh, "Triangle") >>> type(mesh_object.data) <class 'bpy_types.Mesh'> >>> mesh_object.data.name 'Triangle' >>> mesh_object.name 'Triangle' To create non-manifold meshes manually >>> non_manifold_edges = np.array([(0, 3)]) >>> non_manifold_mesh = ddg.arrays.Mesh( ... [(0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)], ... [(0, 1, 2)], ... non_manifold_edges, ... ) >>> non_manifold_mesh_object = ddg.blender.convert( ... non_manifold_mesh, "Triangle with extra edge" ... ) >>> [tuple(e.vertices) for e in non_manifold_mesh_object.data.edges] [(1, 2), (0, 1), (0, 2), (0, 3)] It is usually unnecessary to create the input arrays manually. The functions in `ddg.arrays` convert most `ddg` objects into suitable input for `ddg.blender.convert`. """ collection_ = bpy.context.collection if collection is None else collection bounding_box_ = ddg.arrays._BoundingBox( -np.array(bounding_box), np.array(bounding_box) ) # QSIC case: Circumvent _array conversion as this only works in blender if type(convertible) is ddg.geometry.QuadricIntersection: return convert_qsic( convertible, name, material=material, collection=collection, bounding_box=bounding_box, curve_radius=curve_radius, sampling=curve_sampling, subspace_size=subspace_size, point_radius=point_radius, icosphere_subdivision_steps=icosphere_subdivision_steps, ) arrays = ddg.arrays.convert( convertible, curve_sampling=curve_sampling, surface_sampling=surface_sampling, subspace_size=subspace_size, ) embedded = ddg.arrays._embed(arrays) match embedded: case Points() as p: mesh = _points_to_mesh( Points(np.atleast_2d(p.points)), bounding_box_, _default_point_mesh( icosphere_subdivision_steps, point_radius, ), ) bobj = ddg.blender.mesh.mesh_object( mesh.points, mesh.non_manifold_edges, mesh.faces, name, collection_ ) case Curve() as curve: if bounding_box_.is_R3: # Skip possibly expensive bounding box intersection. bobj = ddg.blender.curve.curve_object( curve.points, curve.periodicity, name, collection_ ) else: bounded_components = ddg.arrays._bounding_box_Curve( curve, bounding_box_ ) bobj = ddg.blender.curve.disconnected_curve_object( bounded_components, name, collection_ ) case CurveList() as curve: if bounding_box_.is_R3: # Skip possibly expensive bounding box intersection. curve_ = curve else: curve_ = ddg.arrays._bounding_box_Curve_DisconnectedCurve( curve, bounding_box_ ) bobj = ddg.blender.curve.disconnected_curve_object( curve_, name, collection_ ) case Mesh(points, faces, non_manifold_edges): bobj = ddg.blender.mesh.mesh_object( points, non_manifold_edges, faces, name, collection_ ) # Only compute possibly expensive bounding box intersection when necessary. if not bounding_box_.is_R3: with ddg.blender.bmesh.bmesh_from_mesh(bobj.data) as bm: ddg.blender.bmesh.cut_bounding_box( bm, bounding_box_.distances, bounding_box_.center ) case _: raise Exception(em.should_never_happen) if isinstance(embedded, Points) or ( isinstance(embedded, Mesh) and isinstance( convertible, (ddg.geometry.Quadric, ddg.geometry.spheres.QuadricSphere) ) ): ddg.blender.mesh.shade_smooth(bobj) if material is not None: ddg.blender.material.set_material(bobj, material) if isinstance(bobj.data, bpy.types.Curve): bobj.data.bevel_depth = curve_radius return bobj
[docs]def scatter( convertible, point_mesh, name, /, material=None, collection=None, curve_sampling=_default_curve_sampling, surface_sampling=_default_surface_sampling, subspace_size=_default_subspace_size, ): """Put a mesh at each vertex position of convertible. Parameters ---------- convertible This must be an instance of:: - `ddg.geometry.Subspace` of dimension <= 2 - `ddg.geometry.Quadric` of dimension <= 2 - `ddg.geometry.spheres.QuadricSphere` of dimension <= 2 - `ddg.indexedfaceset.IndexedFaceSet` with coordinate attribute "co" - `ddg.halfedge.Surface` with coordinate attribute "co" - `ddg.nets.SmoothCurve` - `ddg.nets.SmoothNet` - `ddg.nets.DiscreteCurve` - `ddg.nets.DiscreteNet` - `ddg.nets.PointNet` - `ddg.nets.EmptyNet` - `ddg.nets.NetCollection` of smooth or discrete nets of the same dimension - `ddg.arrays.Points` - `ddg.arrays.Curve` - `ddg.arrays.CurveList` - `ddg.arrays.Mesh` point_mesh: ddg.arrays.Mesh A mesh that will be used for each point. name : str Sets `bobj.name` and `bobj.data.name` to this value. material : str or bpy.types.Material (default=None) The resulting Blender object is assigned this material if it isn't `None`. collection : bpy.types.Collection (default=None) The resulting Blender object is linked to this collection if it isn't `None`. curve_sampling : tuple of an int and a float (default=_default_curve_sampling) Determines the sample number and stepsize for curves surface_sampling : tuple of an int and a float (default=_default_surface_sampling) Determines the sample number and stepsize for surfaces subspace_size : float (default=_default_subspace_size) Determines the size of subspaces Returns ------- bpy.types.Object The `data` attribute is of type bpy.types.Mesh` represents a point or points. Raises ------ TypeError If `convertible` has the wrong type ValueError If convertible cannot be converted to point/points. Examples -------- >>> import bpy >>> import numpy as np >>> import ddg >>> points = ddg.arrays.Points([(1, 2, 3), (3, 4, 5)]) >>> cube = ddg.arrays.Mesh( ... [ ... (0, 0, 0), ... (0, 0, 1), ... (0, 1, 0), ... (0, 1, 1), ... (1, 0, 0), ... (1, 0, 1), ... (1, 1, 0), ... (1, 1, 1), ... ], ... [ ... (0, 1, 2, 3), ... (4, 5, 6, 7), ... (0, 1, 4, 5), ... (2, 3, 6, 7), ... (0, 2, 4, 6), ... (1, 3, 5, 7), ... ], ... ) >>> ddg.blender.scatter(points, cube, "cube_points") bpy.data.objects['cube_points'] """ collection_ = bpy.context.collection if collection is None else collection bounding_box_ = ddg.arrays._BoundingBox(-np.ones(3) * np.inf, np.ones(3) * np.inf) arrays = ddg.arrays.convert( convertible, curve_sampling=curve_sampling, surface_sampling=surface_sampling, subspace_size=subspace_size, ) points = ddg.arrays._to_points(arrays) embedded = ddg.arrays._embed(points) mesh = _points_to_mesh(embedded, bounding_box_, point_mesh) bobj = ddg.blender.mesh.mesh_object( mesh.points, mesh.non_manifold_edges, mesh.faces, name, collection_ ) if material is not None: ddg.blender.material.set_material(bobj, material) return bobj
[docs]def vertices( convertible, name, /, radius=_default_point_radius, icosphere_subdivision_steps=_default_icosphere_subdivision_steps, material=None, collection=None, curve_sampling=_default_curve_sampling, surface_sampling=_default_surface_sampling, subspace_size=_default_subspace_size, ): """Convert pyddg objects and points to points as Blender objects. Parameters ---------- convertible This must be an instance of:: - `ddg.geometry.Subspace` of dimension <= 2 - `ddg.geometry.Quadric` of dimension <= 2 - `ddg.geometry.spheres.QuadricSphere` of dimension <= 2 - `ddg.indexedfaceset.IndexedFaceSet` with coordinate attribute "co" - `ddg.halfedge.Surface` with coordinate attribute "co" - `ddg.nets.SmoothCurve` - `ddg.nets.SmoothNet` - `ddg.nets.DiscreteCurve` - `ddg.nets.DiscreteNet` - `ddg.nets.PointNet` - `ddg.nets.EmptyNet` - `ddg.nets.NetCollection` of smooth or discrete nets of the same dimension - `ddg.arrays.Points` - `ddg.arrays.Curve` - `ddg.arrays.CurveList` - `ddg.arrays.Mesh` name : str Sets `bobj.name` and `bobj.data.name` to this value. radius: float (default=_default_point_radius) Radius of the icosphere mesh. icosphere_subdivision_steps: int (default=_default_icosphere_subdivision_steps) Subdivision steps for the icosphere mesh. material : str or bpy.types.Material (default=None) The resulting Blender object is assigned this material if it isn't `None`. collection : bpy.types.Collection (default=None) The resulting Blender object is linked to this collection if it isn't `None`. curve_sampling : tuple of an int and a float (default=_default_curve_sampling) Determines the sample number and stepsize for curves surface_sampling : tuple of an int and a float (default=_default_surface_sampling) Determines the sample number and stepsize for surfaces subspace_size : float (default=_default_subspace_size) Determines the size of subspaces Returns ------- bpy.types.Object The `data` attribute is of type bpy.types.Mesh` represents a point or points. Raises ------ TypeError If `convertible` has the wrong type ValueError If convertible cannot be converted to point/points. Examples -------- >>> import bpy >>> import numpy as np >>> import ddg >>> point = ddg.arrays.Points((1, 2, 3)) >>> ddg.blender.vertices(point, "one_point") bpy.data.objects['one_point'] >>> points = ddg.arrays.Points([(1, 2, 3), (3, 4, 5)]) >>> ddg.blender.vertices(points, "two_points") bpy.data.objects['two_points'] To change the radius of the visualized points >>> ddg.blender.vertices(points, "radius_points", radius=1.0) bpy.data.objects['radius_points'] """ point_mesh = _default_point_mesh( icosphere_subdivision_steps, radius, ) bobj = scatter( convertible, point_mesh, name, material=material, collection=collection, curve_sampling=curve_sampling, surface_sampling=surface_sampling, subspace_size=subspace_size, ) ddg.blender.mesh.shade_smooth(bobj) return bobj
[docs]def edges( convertible, name, /, radius=_default_curve_radius, material=None, collection=None, curve_sampling=_default_curve_sampling, surface_sampling=_default_surface_sampling, subspace_size=_default_subspace_size, ): """Convert mesh to edges as Blender objects. Parameters ---------- convertible : Object convertible to a ddg.array.Mesh name : str Sets `bobj.name` and `bobj.data.name` to this value. radius : float (default=_default_curve_radius) Sets the radius of the edges as `bobj.data.bevel_depth` material : str or bpy.types.Material (default=None) The resulting Blender object is assigned this material if it isn't `None`. collection : bpy.types.Collection (default=None) The resulting Blender object is linked to this collection if it isn't `None`. curve_sampling : tuple of an int and a float (default=_default_curve_sampling) Determines the sample number and stepsize for curves surface_sampling : tuple of an int and a float (default=_default_surface_sampling) Determines the sample number and stepsize for surfaces subspace_size : float (default=_default_subspace_size) Determines the size of subspaces Returns ------- bpy.types.Object The `data` attribute is of type bpy.types.Mesh` represents the edges. Examples -------- >>> import bpy >>> import numpy as np >>> import ddg >>> mesh = ddg.arrays.convert(ddg.halfedge.dodecahedron()) >>> bobj = ddg.blender.edges(mesh, "Edges of Dodecahedron", radius=0.1) >>> bobj.data.bevel_depth 0.1... """ mesh = ddg.arrays.convert( convertible, curve_sampling=curve_sampling, surface_sampling=surface_sampling, subspace_size=subspace_size, ) if not isinstance(mesh, ddg.arrays.Mesh): raise TypeError( f"{convertible} must be convertible to type ddg.array.Mesh.\n" + "Obtained {type(mesh)}" ) mesh_ = ddg.arrays.Mesh(mesh.points, (), ddg.arrays.edges(mesh)) bobj = convert(mesh_, name, None, collection) with ddg.blender.context.mode(bobj, mode="OBJECT"): bpy.ops.object.select_all(action="DESELECT") bobj.select_set(True) bpy.ops.object.convert(target="CURVE") bobj.select_set(False) ddg.blender.material.set_material(bobj, material) bobj.data.bevel_depth = radius return bobj
[docs]def convert_qsic( intersection_object, name, material=None, collection=None, bounding_box=(np.inf, np.inf, np.inf), sampling=_default_curve_sampling, subspace_size=_default_subspace_size, curve_radius=_default_curve_radius, point_radius=_default_point_radius, icosphere_subdivision_steps=_default_icosphere_subdivision_steps, ): """Converts a ddg.geometry.QuadricIntersection object into a blender object containing the intersection curve of the two quadrics Parameters ---------- intersection_object : ddg.geometry.QuadricIntersection The intersection to depict with attributes Q1, Q2 representing the quadrics that span it name : str Sets `bobj.name` and `bobj.data.name` to this value. material : str or bpy.types.Material (default=None) The resulting Blender object is assigned this material if it isn't `None`. collection : bpy.types.Collection (default=None) The resulting Blender object is linked to this collection if it isn't `None`. bounding_box : array_like of shape (3,) of type float (default=(np.inf, np.inf, np.inf)) Bounding box of the resulting blender object. sampling : tuple of an int and a float (default=_default_curve_sampling) Determines the sample number and stepsize for quadrics used to intersect subspace_size : float (default=_default_subspace_size) Determines the size of subspaces curve_radius : float (default=_default_curve_radius) Sets the bevel depth of the intersection curve to this value point_radius : float (default=_default_point_radius) Radius of the sphere representing points icosphere_subdivision_steps : int (default=_default_icosphere_subdivision_steps) Number of subdivision steps for the icosphere representing points Returns ------- bpy.types.Object """ if intersection_object.dimension_complex != 1: raise TypeError( textwrap.dedent( f"""\ {type(intersection_object)} can only be converted if it is of dimension_complex == 1 . """ ) ) subspaces = None intersection_curves = [] # find out whether one of the quadrics is built from subspaces if ( intersection_object.Q1.rank <= 2 or intersection_object.Q1.signature() == ddg.geometry.signatures.Signature(3, 0, 1) ): subspaces = ddg.geometry.quadric_to_subspaces(intersection_object.Q1) nondegenerate_quadric = intersection_object.Q2 elif ( intersection_object.Q2.rank <= 2 or intersection_object.Q2.signature() == ddg.geometry.signatures.Signature(3, 0, 1) ): subspaces = ddg.geometry.quadric_to_subspaces(intersection_object.Q2) nondegenerate_quadric = intersection_object.Q1 # First case: if one of the quadrics is made up of subspaces, derive the # intersection object from quadric-subspace intersection if subspaces is not None: subspaces = [subspace for subspace in subspaces if not subspace.at_infinity()] for subspace in subspaces: intersect_curve = ddg.geometry.meet(nondegenerate_quadric, subspace) if intersect_curve.dimension != -1: if ( intersect_curve.subspace.dimension != 1 or intersect_curve.dimension != 1 ): try: intersect_bobj = convert( intersect_curve, str(random()), material=material, collection=collection, bounding_box=bounding_box, subspace_size=subspace_size, point_radius=point_radius, icosphere_subdivision_steps=icosphere_subdivision_steps, ) except: intersect_bobj = convert( intersect_curve, str(random()), material=material, collection=collection, subspace_size=subspace_size, point_radius=point_radius, icosphere_subdivision_steps=icosphere_subdivision_steps, ) else: intersect_subspace = intersect_curve.subspace intersect_bobj = convert( intersect_subspace, str(random()), material=material, collection=collection, subspace_size=subspace_size, point_radius=point_radius, icosphere_subdivision_steps=icosphere_subdivision_steps, ) # collect curve objects to join them later intersection_curves += [intersect_bobj] # Second case: both quadrics cannot be written as a collection as subspaces # find the intersection curve as an intersection of the blender meshes if subspaces is None: if isinstance(intersection_object.Q1, ddg.geometry.spheres.QuadricSphere): Q1bobj = convert( intersection_object.Q1, str(random()), curve_sampling=sampling, surface_sampling=sampling, ) else: Q1bobj = convert( intersection_object.Q1, str(random()), curve_sampling=sampling, surface_sampling=sampling, ) if isinstance(intersection_object.Q2, ddg.geometry.spheres.QuadricSphere): Q2bobj = convert( intersection_object.Q2, str(random()), curve_sampling=sampling, surface_sampling=sampling, ) else: Q2bobj = convert( intersection_object.Q2, str(random()), curve_sampling=sampling, surface_sampling=sampling, ) if collection is None: # mesh.intersection needs a collection collection = "Intersection" intersection_curves = [ ddg.blender.mesh.intersection( Q1bobj, Q2bobj, collection=collection, intersection_operator=lambda: bpy.ops.mesh.intersect( mode="SELECT", solver="FAST", ), ) ] # cleanup delete(Q1bobj) delete(Q2bobj) for curve in intersection_curves: # give the curve the correct bevel depth and name if curve is not None and isinstance(curve.data, bpy.types.Curve): curve.data.bevel_depth = curve_radius # turn all curves into blender objects with ddg.blender.context.mode(curve, mode="OBJECT"): bpy.ops.object.select_all(action="DESELECT") curve.select_set(True) bpy.ops.object.convert(target="MESH") curve.select_set(False) # if there are intersection objects that can't be expressed as a curve, add them to # the curve as a complete intersection mesh list_of_objects = [obj for obj in intersection_curves if obj is not None] if list_of_objects: intersection_curve = ddg.blender.mesh.join( list_of_objects, name, collection=collection, keep_original=False ) with ddg.blender.bmesh.bmesh_from_mesh(intersection_curve.data) as bm: ddg.blender.bmesh.cut_bounding_box(bm, bounding_box) else: intersection_curve = None if material is not None and intersection_curve is not None: ddg.blender.material.set_material(intersection_curve, material) return intersection_curve