This function should fulfil your purpose, with a minor alteration required if you wish to use step_size instead of npoints (they'll be evenly distributed either way).
import matplotlib.pyplot as plt
import numpy as np
def plot_circle_3d(
ax: plt.Axes,
center: tuple[float, float, float],
normal: tuple[float, float, float],
radius: float,
npoints: int,
**kwargs
) -> None:
t = np.linspace(start=0, stop=2 * np.pi, num=npoints)
flat_circle = np.array(
[radius * np.cos(t), radius * np.sin(t), np.zeros(np.shape(t))]
)
if np.isclose(normal[0], 0) and np.isclose(normal[1], 0):
# Transform matrix breaks if normal vector is [0, 0, n]
tilted_circle = flat_circle
else:
Nt = _calculate_normal_transform(normal=normal)
tilted_circle = np.matmul(Nt, flat_circle)
circle = tilted_circle + np.expand_dims(center, axis=1)
ax.plot(xs=circle[0], ys=circle[1], zs=circle[2], **kwargs)
def _calculate_normal_transform(normal: tuple[float, float, float]) -> np.ndarray:
x, y, z = normal
a = np.sqrt(x**2 + y**2)
b = np.sqrt(x**2 + y**2 + z**2)
return np.array(
[
[
(x**2 * z + y**2 * b) / (a**2 * b),
x * y * (z - b) / (a**2 * b),
x / b,
],
[
x * y * (z - b) / (a**2 * b),
(x**2 * b + y**2 * z) / (a**2 * b),
y / b,
],
[-x / b, -y / b, z / b],
]
)
fig = plt.figure()
ax = fig.add_subplot(projection="3d")
plot_circle_3d(ax=ax, center=(2, 5, 7), normal=(1, 2, 1), radius=5, npoints=1000)
plt.show()
3d circle