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