Source code for ddg.blender.curve

import bpy
import numpy as np
from numpy import cos, pi, sin, sqrt, tan

import ddg._error_messages as em
from ddg.arrays import CurveList, _parse_periodicity, _parse_points
from ddg.blender.mesh import mesh
from ddg.blender.object import blender_data_to_object, link

###################################################
#   create and add  generic curves, set properties
###################################################


def _insert(x, i, value):
    n, m = x.shape
    value_ = value if isinstance(value, np.ndarray) else value * np.ones((n, 1))
    if i == m or i == -1:
        return np.column_stack((x, value_))
    else:
        return np.insert(x, i, value_, 1)


def _homogenise(x, i):
    return _insert(x, i, 1)


def _add_spline(curve, points, periodicity, /):
    n = points.shape[0]

    spline = curve.splines.new("POLY")
    spline.points.add(n - 1)
    points_with_weights = _homogenise(points, -1)
    for i, p in enumerate(points_with_weights):
        spline.points[i].co = p
    spline.use_cyclic_u = periodicity


[docs]def curve(name, points, periodic): """Create a Blender curve from points. Parameters ---------- name : str The `name` attribute of the curve. points : array_like of shape (num_points, 3) The points of the curve. The dtype must be some kind of floating point dtype. periodic : bool The curve is closed if and only if periodic is `True`. Returns ------- bpy.types.Curve Raises ------ ValueError If there already exists a curve with the given name. """ name_error = ( em.str_(f"bpy.data.curves already contains a curve whose name is '{name}'.") if name in bpy.data.curves else None ) points_ = _parse_points(points, "points") periodicity_ = _parse_periodicity(periodic, "periodic") if ( isinstance(name_error, em.str_) or isinstance(points_, em.str_) or isinstance(periodicity_, em.str_) ): raise ValueError(em.concatenate_messages(name_error, points_, periodicity_)) else: curve = bpy.data.curves.new(name, "CURVE") curve.dimensions = "3D" _add_spline(curve, points_, periodicity_) return curve
[docs]def disconnected_curve(name, curve_components, /): """Create a Blender curve from connected components. Parameters ---------- name : str The `name` attribute of the curve. curve_components : ddg.arrays.DisconnectedCurve Returns ------- bpy.types.Curve Raises ------ ValueError If there already exists a curve with the given name. """ if name in bpy.data.curves: raise ValueError( f"bpy.data.curves already contains a curve whose name is '{name}'." ) curve_ = CurveList(curve_components) curve_data = bpy.data.curves.new(name, "CURVE") curve_data.dimensions = "3D" for c in curve_: _add_spline(curve_data, c.points, c.periodicity) return curve_data
[docs]def curve_as_mesh(name, points): """Create a Blender curve from points and convert it to a curve. Parameters ---------- name : str The `name` attribute of the mesh. points : array_like of shape (num_points, 3) The points of the curve/mesh. The dtype must be some kind of floating point dtype. Returns ------- bpy.types.Mesh See also -------- ddg.blender.mesh.mesh ddg.blender.curve.curve """ name_error = ( em.str_(f"bpy.data.curves already contains a curve whose name is '{name}'.") if name in bpy.data.curves else None ) points_ = _parse_points(points, "points") if isinstance(name_error, em.str_) or isinstance(points_, em.str_): raise ValueError(em.concatenate_messages(name_error, points_)) n = points_.shape[0] segments = np.column_stack((np.arange(n - 1), np.arange(1, n))) return mesh(name, points, np.zeros((0, 2), int), segments)
[docs]def curve_object( points, periodic, curve_and_object_name, collection, ): """Create a curve from points and wrap it in a Blender object. Parameters ---------- points : array_like of shape (num_points, 3) The points of the curve. The dtype must be some kind of floating point dtype. periodic : bool The curve is closed if and only if periodic is `True`. curve_and_object_name : str or tuple[str, str] The `name` attribute of the curve and the object. If only one string is passed, then use the same name for both. collection : bpy.types.Collection, str, None If `None`, then the Blender object is not linked. Otherwise, the Collection or name of collection to link the object to. Returns ------- bpy.types.Object Raises ------ ValueError If there already exists a Blender curve or Blender object with the same name. """ if isinstance(curve_and_object_name, str): c_name = curve_and_object_name object_name = curve_and_object_name else: c_name, object_name = curve_and_object_name curve_name_error = ( em.str_(f"bpy.data.curves already contains a curve whose name is '{c_name}'.") if c_name in bpy.data.curves else None ) object_name_error = ( em.str_( f"bpy.data.objects already contains a object whose name is '{object_name}'." ) if object_name in bpy.data.objects else None ) points_ = _parse_points(points, "points") periodicity_ = _parse_periodicity(periodic, "periodic") if ( isinstance(curve_name_error, em.str_) or isinstance(object_name_error, em.str_) or isinstance(points_, em.str_) or isinstance(periodicity_, em.str_) ): raise ValueError( em.concatenate_messages( curve_name_error, object_name_error, points_, periodicity_ ) ) else: c = curve(c_name, points_, periodicity_) bobj = blender_data_to_object(object_name, c) if collection is not None: link(bobj, collection) return bobj
[docs]def disconnected_curve_object(curve_components, curve_and_object_name, collection, /): """Create a Blender curve from connected components. Parameters ---------- curve_components : sequence of ddg.arrays.Curve curve_and_object_name : str or tuple[str, str] The `name` attribute of the curve and the object. If only one string is passed, then use the same name for both. collection : bpy.types.Collection, str, None If `None`, then the Blender object is not linked. Otherwise, the Collection or name of collection to link the object to. Returns ------- bpy.types.Object Raises ------ ValueError If there already exists a Blender curve or Blender object with the same name. """ if isinstance(curve_and_object_name, str): c_name = curve_and_object_name object_name = curve_and_object_name else: c_name, object_name = curve_and_object_name curve_name_error = ( em.str_(f"bpy.data.curves already contains a curve whose name is '{c_name}'.") if c_name in bpy.data.curves else None ) object_name_error = ( em.str_( f"bpy.data.objects already contains a object whose name is '{object_name}'." ) if object_name in bpy.data.objects else None ) curve_components_ = curve_components if isinstance(curve_name_error, em.str_) or isinstance(object_name_error, em.str_): raise ValueError(em.concatenate_messages(curve_name_error, object_name_error)) else: c = disconnected_curve(c_name, curve_components_) bobj = blender_data_to_object(object_name, c) if collection is not None: link(bobj, collection) return bobj
[docs]def create_curve( coordinates, name="Curve", type_="POLY", with_weights=False, cyclic=None, curve=None ): """Create a curve object from given coordinates. Parameters ---------- coordinates : numpy.ndarray of shape (n,i) or list of such numpy.ndarray Array containing the coordinates of the splines. * A single 2D array means a single spline, a list of 2D arrays means a collection of splines. * `i` is 4 if `type` is "POLY" and `with_weights` is True. Otherwise `i` is 3. * `n` is the number of (control) points of each curve. Can be different for each curve. name : str (default="Curve") Name of the new curve object. type_ : {"POLY", "BEZIER", "NURBS"}, (default="POLY") Type of the created splines. with_weights : bool (default=False) Specify whether weights are given. * This is only checked if `type` is "POLY". * If False, the weights will default to 1.0. cyclic : list of bool (default=None) Specify which splines are cyclic (defaulting to non-cyclic). curve : bpy.types.Curve (default=None) When given, the curve object to store the created data Returns ------- bpy.types.Curve """ if curve is None: curvedata = bpy.data.curves.new(name=name, type="CURVE") curvedata.dimensions = "3D" else: curvedata = curve curvedata.splines.clear() # determine whether coordinates are given for multiple curves # We can't do len(np.shape(coordinates)) == 2 because coordinates might be # a ragged array and creating an array from ragged data without specifying # dtype=object is deprecated since numpy 1.19.0. The previous solution # probably only worked accidentally because the ragged array will have # shape (n,) and not (n,m,i) as was probably assumed. if len(np.shape(coordinates[0])) == 1: coordinates = [coordinates] if cyclic is None: cyclic = [False for i in range(len(coordinates))] for curve_coords, cyc in zip(coordinates, cyclic): spline = curvedata.splines.new(type_) if type_ == "BEZIER": spline.bezier_points.add(len(curve_coords) - 1) for i, c in enumerate(curve_coords): if with_weights: c = c[0:3] bezier_point = spline.bezier_points[i] bezier_point.co = c bezier_point.handle_left_type = "AUTO" bezier_point.handle_right_type = "AUTO" else: spline.points.add(len(curve_coords) - 1) for i, c in enumerate(curve_coords): if not with_weights: c = np.append(c, 1.0) spline.points[i].co = c spline.use_cyclic_u = cyc if type_ == "NURBS": spline.order_u = len(spline.points) - 1 spline.use_endpoint_u = True return curvedata
[docs]def add_curve(curvedata, name="Curve", location=(0.0, 0.0, 0.0), collection=None): """Add a `bpy.types.Curve` object to a collection. Parameters ---------- curvedata : bpy.types.Curve Curve to add. name : str (default="Curve") Name of the `bpy.types.Object` object. location : triple of float (default=(0.0, 0.0, 0.0)) Location of the new `bpy.types.Object` object. collection : bpy.types.Collection, optional Collection to add the curve to. Returns ------- bpy.types.Object The new created object containing the curve. Notes ----- * The `collection` option defaults to `bpy.context.scene.collection`. """ objectdata = bpy.data.objects.new(name, curvedata) objectdata.location = location if not collection: collection = bpy.context.scene.collection collection.objects.link(objectdata) return objectdata
[docs]def new_bezier_curve(ctr_pts, handles, closed=False, name="Curve", collection=None): """Create a new bezier curve. Parameters ---------- ctr_pts : Iterable of triples of float Control points of the bezier curve. handles : Iterable of tuples of triples of float Left and right handles at the control points. closed : bool (default=False) Specify whether curve is closed. name : str (default="Curve") Name of the `bpy.types.Curve` and `bpy.types.Object` object. collection : bpy.types.Collection (default=None) Collection the curve shall be linked to. Returns ------- bpy.types.Curve or (bpy.types.Object, bpy.types.Curve) The created curve object if `collection` is None. Else the curve object and the object linked to `collection` containing it. See Also -------- new_bezier_circle, new_bezier_arc """ curve = bpy.data.curves.new(name, "CURVE") curve.dimensions = "3D" spline = curve.splines.new("BEZIER") spline.use_cyclic_u = closed add_bezier_points(spline, ctr_pts, handles, new=True) if collection: obj = bpy.data.objects.new(name, curve) collection.objects.link(obj) collection.update() return (obj, curve) else: return curve
[docs]def set_curve_properties(curve, **properties): """Set curve properties for a Blender curve. Parameters ---------- curve : bpy.types.Object or bpy.types.Curve Curve to set properties. **properties : keyword arguments Properties to set. """ if not isinstance(curve, bpy.types.Curve): curve = curve.data for option in properties: setattr(curve, option, properties[option])
################################################### # create specific curves ###################################################
[docs]def create_circle_data( name="Circle", radius=1.0, location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0) ): """Create a new bezier circle using standard `bpy.ops` methods. Creates a new `bpy.types.Curve` of bezier type, wraps it in a `bpy.types.Object` and links this object to `bpy.context.scene.collection`. Parameters ---------- name : str (default="Circle") Name of the created objects. radius : float (default=1.0) Radius of the circle. location : triple of float (default=(0.0, 0.0, 0.0)) Center of the `bpy.types.Object` object. rotation : triple of float (default=(0.0, 0.0, 0.0)) Rotation of the `bpy.types.Object` object. Returns ------- bpy.types.Curve """ bpy.ops.curve.primitive_bezier_circle_add( radius=radius, location=location, rotation=rotation ) circleObject = bpy.context.active_object circleGeometry = circleObject.data bpy.data.objects.remove(circleObject) circleGeometry.name = name circleGeometry.materials.append(None) return circleGeometry
[docs]def new_bezier_circle(trafo=np.eye(4), collection=None): """Create a new bezier circle. Parameters ---------- trafo : numpy ndarray (default=numpy.eye(4)) Similarity transformation applied to the circle before adding. collection : bpy.types.Collection (default=None) Collection the curve shall be linked to. Returns ------- bpy.types.Curve or (bpy.types.Object, bpy.types.Curve) The created curve object if `collection` is None. Else the curve object and the object linked to `collection` containing it. See Also -------- new_bezier_arc Notes ----- * The circle lies in the xy-plane before applying `trafo`. * The first control point will be `(1, 0, 0)` before applying `trafo`. * The circle is oriented in counterclockwise direction per default. """ ctr_pts = [(1, 0, 0), (0, 1, 0), (-1, 0, 0), (0, -1, 0)] tmp = 4 * (sqrt(2) - 1) / 3 handles = [ [(1, -tmp, 0), (1, tmp, 0)], [(tmp, 1, 0), (-tmp, 1, 0)], [(-1, tmp, 0), (-1, -tmp, 0)], [(-tmp, -1, 0), (tmp, -1, 0)], ] ctr_pts = [np.dot(trafo, v) for v in ctr_pts] handles = [[np.dot(trafo, left), np.dot(trafo, right)] for left, right in handles] return new_bezier_curve( ctr_pts, handles, closed=True, name="Circle", collection=collection )
[docs]def new_bezier_arc( phi, nbr_ctr_pts="AUTO", trafo=np.eye(4), reverse=False, collection=None ): """Create a circular arc bezier curve. Parameters ---------- phi : float Angle of the circular arc. nbr_ctr_pts : int (default="AUTO") Number of control points evenly spaced on the circular arc. trafo : numpy.ndarray (default=numpy.eye(4)) Similarity transformation applied to the circle before adding. reverse : bool (default=False) Reverse the orientation of added circle. collection : bpy.types.Collection, optional Collection the curve shall be linked to. Returns ------- bpy.types.Curve or (bpy.types.Object, bpy.types.Curve) The created curve object if `collection` is None. Else the curve object and the object linked to `collection` containing it. See Also -------- new_bezier_circle Notes ----- * The circle lies in the xy-plane before applying `trafo`. * The first control point will be `(1,0,0)` before applying `trafo`. * The circle is oriented in counterclockwise direction per default. """ if nbr_ctr_pts == "AUTO": nbr_ctr_pts = int((2 * phi) / pi) + 2 ctr_pts, handles, handle_dist = [], [], (4 / 3) * tan(phi / (2 * nbr_ctr_pts)) for phi in np.linspace(0, phi, nbr_ctr_pts): v = np.array((cos(phi), sin(phi), 0)) n = np.cross(v, np.array((0, 0, 1))) handle_left = v - handle_dist * n handle_right = v + handle_dist * n ctr_pts.append(v) handles.append((handle_left, handle_right)) handles[0][0], handles[-1][-1] = ctr_pts[0], ctr_pts[-1] ctr_pts = [np.dot(trafo, v) for v in ctr_pts] handles = [[np.dot(trafo, left), np.dot(trafo, right)] for left, right in handles] if reverse: ctr_pts = ctr_pts[::-1] handles = [handle[::-1] for handle in handles[::-1]] return new_bezier_curve(ctr_pts, handles, name="Arc", collection=collection)
################################################### # modify generic curves ###################################################
[docs]def add_bezier_points(spline, ctr_pts, handles, new=False): """Add control points to a bezier spline. Parameters ---------- spline : bpy.types.Spline Spline to operate on. ctr_pts : Iterable of triple of float Control points to add. handles : Iterable of tuple of triple of float Left and right handles at the control points. new : bool (default=False) Is the spline newly created and contains no old data. See Also -------- glue_bezier_points Notes ----- * This function CHANGES the given spline `spline`. * The control points and handles are paired by index. * The first item of a handle is the left handle and the second is the right. """ if new: old_len = 0 spline.bezier_points.add(len(ctr_pts) - 1) else: old_len = len(spline.bezier_points) spline.bezier_points.add(len(ctr_pts)) for pt, co, handle in zip(spline.bezier_points[old_len:], ctr_pts, handles): pt.co = co pt.handle_left, pt.handle_right = handle
[docs]def glue_bezier_points(spline, ctr_pts, handles): """Add control points to a spline and match the handles. Parameters ---------- spline : bpy.types.Spline Spline to operate on. ctr_pts : Iterable of triple of float Control points to add. handles : Iterable of tuple of triple of float Left and right handles at the control points. See Also -------- add_bezier_points Notes ----- * This function CHANGES the given spline `spline`. * The spline `spline` has to contain at least one control point. * The function overrides the right handle of the last control point of `spline`. * `handles` has to contain one more item then `ctr_pts`. """ bez_pts, old_len = spline.bezier_points, len(spline.bezier_points) bez_pts.add(len(ctr_pts)) bez_pts[old_len - 1].handle_right = handles[0][1] for pt, co, handle in zip(bez_pts[old_len:], ctr_pts, handles[1:]): pt.co = co pt.handle_left, pt.handle_right = handle
################################################### # modify specific curves ###################################################
[docs]def add_circle_points(spline, nbr_ctr_pts=4, trafo=np.eye(4), reverse=False): """Add a circle to a spline. Parameters ---------- spline : bpy.types.Spline Spline to operate on. nbr_ctr_pts : int (default=4) Number of control points evenly spaced on the circle. trafo : numpy.ndarray (default=numpy.eye(4)) Similarity 4x4 transformation applied to the circle before adding. reverse : bool (default=False) Reverse the orientation of added circle. See Also -------- add_arc_points, glue_circle_points Notes ----- * This function CHANGES the given spline `spline`. * The circle lies in the xy-plane before applying `trafo`. * The first control point will be `(1,0,0)` before applying `trafo`. * The circle is oriented in counterclockwise direction per default. """ ctr_pts, handles, handle_dist = [], [], (4 / 3) * tan(pi / (2 * nbr_ctr_pts)) for phi in np.linspace(0, 2 * pi, nbr_ctr_pts + 1): v = np.array((cos(phi), sin(phi), 0)) n = np.cross(v, np.array((0, 0, 1))) handle_left = v - handle_dist * n handle_right = v + handle_dist * n ctr_pts.append(v) handles.append((handle_left, handle_right)) handles[0][0], handles[-1][-1] = ctr_pts[0], ctr_pts[-1] ctr_pts = [np.dot(trafo, v) for v in ctr_pts] handles = [[np.dot(trafo, left), np.dot(trafo, right)] for left, right in handles] if reverse: ctr_pts = ctr_pts[::-1] handles = [handle[::-1] for handle in handles[::-1]] add_bezier_points(spline, ctr_pts, handles)
[docs]def add_arc_points(spline, phi, nbr_ctr_pts="AUTO", trafo=np.eye(4), reverse=False): """Add a circular arc to a spline. Parameters ---------- spline : bpy.types.Spline Spline to operate on. phi : float Angle of the circular arc. nbr_ctr_pts : int (default="AUTO") Number of control points evenly spaced on the circular arc. trafo : numpy.ndarray (default=numpy.eye(4)) Similarity transformation applied to the circle before adding. reverse : bool (default=False) Reverse the orientation of added circle. See Also -------- add_circle_points, glue_arc_points Notes ----- * This function CHANGES the given spline `spline`. * The circle lies in the xy-plane before applying `trafo`. * The first control point will be `(1,0,0)` before applying `trafo`. * The circle is oriented in counterclockwise direction per default. """ if nbr_ctr_pts == "AUTO": nbr_ctr_pts = int((2 * phi) / pi) + 2 ctr_pts, handles, handle_dist = [], [], (4 / 3) * tan(phi / (2 * nbr_ctr_pts)) for phi in np.linspace(0, phi, nbr_ctr_pts): v = np.array((cos(phi), sin(phi), 0)) n = np.cross(v, np.array((0, 0, 1))) handle_left = v - handle_dist * n handle_right = v + handle_dist * n ctr_pts.append(v) handles.append((handle_left, handle_right)) handles[0][0], handles[-1][-1] = ctr_pts[0], ctr_pts[-1] ctr_pts = [np.dot(trafo, v) for v in ctr_pts] handles = [[np.dot(trafo, left), np.dot(trafo, right)] for left, right in handles] if reverse: ctr_pts = ctr_pts[::-1] handles = [handle[::-1] for handle in handles[::-1]] add_bezier_points(spline, ctr_pts, handles)
[docs]def glue_circle_points(spline, nbr_ctr_pts=4, trafo=np.eye(4), reverse=False): """Add a circle to a spline and match the handles. Parameters ---------- spline : bpy.types.Spline Spline to operate on. nbr_ctr_pts : int (default=4) Number of control points evenly spaced on the circel. trafo : numpy ndarray (default=numpy.eye(4)) Similarity transformation applied to the circle before adding. reverse : bool (default=False) Reverse the orientation of added circle. See Also -------- glue_arc_points, add_circle_points Notes ----- * This function CHANGES the given spline `spline`. * The circle lies in the xy-plane before applying `trafo`. * The first control point will be `(1,0,0)` before applying `trafo`. * The circle is oriented in counterclockwise direction per default. """ ctr_pts, handles, handle_dist = [], [], (4 / 3) * tan(pi / (2 * nbr_ctr_pts)) for phi in np.linspace(0, 2 * pi, nbr_ctr_pts + 1): v = np.array((cos(phi), sin(phi), 0)) n = np.cross(v, np.array((0, 0, 1))) handle_left = v - handle_dist * n handle_right = v + handle_dist * n ctr_pts.append(v) handles.append((handle_left, handle_right)) handles[0][0], handles[-1][-1] = ctr_pts[0], ctr_pts[-1] ctr_pts = [np.dot(trafo, v) for v in ctr_pts] handles = [[np.dot(trafo, left), np.dot(trafo, right)] for left, right in handles] glue_bezier_points(spline, ctr_pts[1:], handles)
[docs]def glue_arc_points(spline, phi, nbr_ctr_pts="AUTO", trafo=np.eye(4), reverse=False): """Add a circular arc to a spline and match the handles. Parameters ---------- spline : bpy.types.Spline Spline to operate on. phi : float Angle of the circular arc. nbr_ctr_pts : int (default="AUTO") Number of control points evenly spaced on the circular arc. trafo : numpy.ndarray (default=numpy.eye(4)) Similarity transformation applied to the circle before adding. reverse : bool (default=False) Reverse the orientation of added circle. See Also -------- glue_circle_points, add_arc_points Notes ----- * This function CHANGES the given spline `spline`. * The circle lies in the xy-plane before applying `trafo`. * The first control point will be `(1,0,0)` before applying `trafo`. * The circle is oriented in counterclockwise direction per default. """ if nbr_ctr_pts == "AUTO": nbr_ctr_pts = int((2 * phi) / pi) + 2 ctr_pts, handles, handle_dist = [], [], (4 / 3) * tan(phi / (2 * nbr_ctr_pts)) for phi in np.linspace(0, phi, nbr_ctr_pts): v = np.array((cos(phi), sin(phi), 0)) n = np.cross(v, np.array((0, 0, 1))) handle_left = v - handle_dist * n handle_right = v + handle_dist * n ctr_pts.append(v) handles.append((handle_left, handle_right)) handles[0][0], handles[-1][-1] = ctr_pts[0], ctr_pts[-1] ctr_pts = [np.dot(trafo, v) for v in ctr_pts] handles = [[np.dot(trafo, left), np.dot(trafo, right)] for left, right in handles] if reverse: ctr_pts = ctr_pts[::-1] handles = [handle[::-1] for handle in handles[::-1]] glue_bezier_points(spline, ctr_pts[1:], handles)
[docs]def clear(curves=None, do_unlink=True): """Delete all given curves. Parameters ---------- curves : Iterable of bpy.type.Curve (default=True) Curves to be deleted. If argument is not provided or None all curves will be deleted. do_unlink : bool (default=True) Unlink curves from their scenes, if needed, before deleting them. """ if curves is None: curves = bpy.data.curves curves = list(curves) while curves: bpy.data.curves.remove(curves.pop(), do_unlink=do_unlink)