import bpy
import numpy as np
from numpy import pi, sqrt, cos, sin, tan
###################################################
# create and add generic curves, set properties
###################################################
[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.), collection=None):
"""Add a `bpy.types.Curve` object to a collection.
Parameters
----------
curvedata : bpy.types.Curve
Curve to add.
name : str, optional
Name of the `bpy.types.Object` object.
location : triple of float, optional
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, optional
Specify whether curve is closed.
name : str, optional
Name of the `bpy.types.Curve` and `bpy.types.Object` object, respecively.
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, 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.
Returns
-------
None
"""
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.,
location=(0., 0., 0.), rotation=(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, optional
Name of the created objects.
radius : float, optional
Radius of the circle.
location : triple of float
Center of the `bpy.types.Object` object.
rotation : triple of float
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, optional
Similarity transformation applied to the circle before adding.
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_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, optinal
Number of control points evenly spaced on the circular arc.
trafo : numpy ndarray, optional
Similarity transformation applied to the circle before adding.
reverse : bool, optional
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, optional
Is the spline newly created and contains no old data.
Returns
-------
None
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.
Returns
-------
None
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, optinal
Number of control points evenly spaced on the circle.
trafo : numpy ndarray, optional
Similarity 4x4 transformation applied to the circle before adding.
reverse : bool, optional
Reverse the orientation of added circle.
Returns
-------
None
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, optinal
Number of control points evenly spaced on the circular arc.
trafo : numpy ndarray, optional
Similarity transformation applied to the circle before adding.
reverse : bool, optional
Reverse the orientation of added circle.
Returns
-------
None
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, optinal
Number of control points evenly spaced on the circel.
trafo : numpy ndarray, optional
Similarity transformation applied to the circle before adding.
reverse : bool, optional
Reverse the orientation of added circle.
Returns
-------
None
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, optinal
Number of control points evenly spaced on the circular arc.
trafo : numpy ndarray, optional
Similarity transformation applied to the circle before adding.
reverse : bool, optional
Reverse the orientation of added circle.
Returns
-------
None
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)