"""
Geometric Forms
===============
This module contains functions to generate and manipulate basic geometric forms using PyVista. These forms can be used for visualization and analysis in various engineering and physics simulations. The functions in this module provide simple representations of common mechanical components such as control arms, tubes, cylinders, spheres, springs, and other structures.
Each function creates geometric shapes by connecting specified points in 3D space, allowing users to model complex systems efficiently. These forms can be used to build assemblies or test various configurations, making them useful for mechanical simulations, 3D modeling, and CAD systems.
The generated shapes are returned as `pv.MultiBlock` objects, which allow for efficient handling and visualization of multiple geometric forms in a single structure.
These functions provide an easy way to generate basic geometric components for more complex 3D models.
"""
import pyvista as pv
import numpy as np
[docs]
def control_arm(point_a, point_b, common_point, radius=10, resolution=100, n_sides=10):
"""
Generate a control arm (V-shape structure).
Parameters
----------
point_a : array-like
Coordinates of the first base point of the control arm.
point_b : array-like
Coordinates of the second base point of the control arm.
common_point : array-like
Coordinates of the common apex point of the control arm.
radius : float, optional
Radius of the Tubes, by default 10.
resolution : int, optional
Resolution of the Tubes, by default 100.
n_sides : int, optional
Number of sides of the Tubes, by default 10.
Returns
-------
pv.MultiBlock
MultiBlock containing two Tubes representing the control arm.
Notes
-----
The control arm is formed by two Tubes connecting the base points
to the common apex point.
Examples
--------
Create a control arm mapping between three coordinates and show it using PyVista.
>>> import numpy as np
>>> import pyvista as pv
>>> from pymycar.Cad import control_arm
>>> a = control_arm(np.array([586.7, -314.5, 199.9]), np.array([930.7, -230.2, 244.2]), np.array([953.0, -474.2, 272.2]), radius=10, resolution=100, n_sides=10)
>>> plotter = pv.Plotter()
>>> plotter.add_mesh(a, color='red', name="Control Arm")
>>> plotter.add_title("Control Arm")
>>> plotter.show()
"""
e1 = pv.Tube(pointa=point_a, pointb=common_point, resolution=resolution, radius=radius, n_sides=n_sides)
e2 = pv.Tube(pointa=point_b, pointb=common_point, resolution=resolution, radius=radius, n_sides=n_sides)
return pv.MultiBlock([e1, e2])
[docs]
def dashed_tube(point_a, point_b, radius=5, n_dashes=10, duty_cycle=0.5):
"""
Create a dashed tube between two points using multiple short tube segments.
Parameters
----------
point_a : array-like
point_b : array-like
radius : float
n_dashes : int
Number of visible dash segments.
duty_cycle : float
Fraction of each dash period that is "on".
"""
point_a = np.array(point_a)
point_b = np.array(point_b)
direction = point_b - point_a
length = np.linalg.norm(direction)
if length == 0:
raise ValueError("point_a and point_b cannot be the same point")
direction = direction / length
# total segments (on + off)
n_segments = n_dashes * 2
segments = []
for i in range(n_segments):
t0 = i / n_segments
t1 = (i + 1) / n_segments
# keep only "on" segments
if i % 2 == 0:
start = point_a + direction * length * t0
end = point_a + direction * length * t1
seg = pv.Tube(
pointa=start,
pointb=end,
radius=radius,
resolution=50,
n_sides=10
)
segments.append(seg)
return pv.MultiBlock(segments)
[docs]
def dashed_line(point_a, point_b, n_dashes=100):
"""
Create a dashed 3D line between two points using line segments.
"""
point_a = np.array(point_a)
point_b = np.array(point_b)
direction = point_b - point_a
length = np.linalg.norm(direction)
if length == 0:
raise ValueError("point_a and point_b cannot be the same point")
direction = direction / length
n_segments = n_dashes * 2 # on/off pattern
lines = []
for i in range(n_segments):
if i % 2 == 0: # "on" segment
t0 = i / n_segments
t1 = (i + 1) / n_segments
p0 = point_a + direction * length * t0
p1 = point_a + direction * length * t1
line = pv.Line(p0, p1)
lines.append(line)
return pv.MultiBlock(lines)
[docs]
def simple_tube(point_a, point_b, radius=5, resolution=100, n_sides=10):
"""
Generate a simple tube.
Parameters
----------
point_a : array-like
Coordinates of the first point of the tube.
point_b : array-like
Coordinates of the second point of the tube.
radius : float, optional
Radius of the Tube, by default 5.
resolution : int, optional
Resolution of the Tube, by default 100.
n_sides : int, optional
Number of sides of the Tube, by default 10.
Returns
-------
pv.MultiBlock
MultiBlock containing a Tube representing the simple tube.
Notes
-----
The simple tube is formed by a single Tube connecting point_a and point_b.
Examples
--------
Create a tube between two coordinates and show it using PyVista.
>>> import numpy as np
>>> import pyvista as pv
>>> from pymycar.Cad import simple_tube
>>> tube = simple_tube(np.array([934.2, -192.1, 81.2]), np.array([1027.1, -513.7, 43.6]), radius=10, resolution=100, n_sides=10)
>>> plotter = pv.Plotter()
>>> plotter.add_mesh(tube, color='blue', name="Tie Rod Tube")
>>> plotter.add_title("Tie Rod Tube")
>>> plotter.show()
"""
e5 = pv.Tube(pointa=point_a, pointb=point_b, resolution=resolution, radius=radius, n_sides=n_sides)
return pv.MultiBlock([e5])
[docs]
def simple_cylinder(center, height, radius):
"""
Generate a simple cylinder.
Parameters
----------
center : array-like
Coordinates of the center of the cylinder.
height : float
Height of the cylinder.
radius : float
Radius of the cylinder.
Returns
-------
pv.MultiBlock
MultiBlock containing a Cylinder representing the simple cylinder.
Notes
-----
The simple cylinder is a Cylinder centered at the specified point with the given height and radius.
Examples
--------
Create a cylinder at a specific coordinate and show it using PyVista.
>>> import numpy as np
>>> import pyvista as pv
>>> from pymycar.Cad import simple_cylinder
>>> b = simple_cylinder(np.array([941.5, -580.2, 155.1]), height=10, radius=5)
>>> plotter = pv.Plotter()
>>> plotter.add_mesh(b, color='green', name="Wheel Center Cylinder")
>>> plotter.add_title("Wheel Center Cylinder")
>>> plotter.show()
"""
wheel = pv.Cylinder(center=center, direction=(0, 1, 0), height=height, radius=radius)
return pv.MultiBlock([wheel])
[docs]
def cylinder_from_two_points(center, point_a, point_b, height, radius):
"""
Create a PyVista cylinder whose direction is defined by two points.
Parameters
----------
center : array-like
Cylinder center [x, y, z].
point_a : array-like
First point defining the direction.
point_b : array-like
Second point defining the direction.
height : float
Cylinder height.
radius : float
Cylinder radius.
Returns
-------
pyvista.PolyData
Cylinder mesh.
"""
direction = point_b - point_a
norm = np.linalg.norm(direction)
if norm == 0:
raise ValueError("point_a and point_b cannot be the same point (direction length is zero).")
direction = direction / norm
cylinder = pv.Cylinder(
center=center,
direction=direction,
height=height,
radius=radius
)
return pv.MultiBlock([cylinder])
[docs]
def simple_sphere(center, radius):
"""
Generate a simple sphere.
Parameters
----------
center : array-like
Coordinates of the center of the sphere.
radius : float
Radius of the sphere.
Returns
-------
pv.MultiBlock
MultiBlock containing a Sphere representing the simple sphere.
Notes
-----
The simple sphere is a Sphere centered at the specified point with the given radius.
Examples
--------
Create a sphere at a specific coordinate and show it using PyVista.
>>> import numpy as np
>>> import pyvista as pv
>>> from pymycar.Cad import simple_sphere
>>> c = simple_sphere(np.array([941.5, -580.2, 155.1]), radius=10)
>>> plotter = pv.Plotter()
>>> plotter.add_mesh(c, color='yellow', name="Wheel Center Sphere")
>>> plotter.add_title("Wheel Center Sphere")
>>> plotter.show()
"""
point_wheel_center = pv.Sphere(radius=radius, center=center, theta_resolution=30, phi_resolution=30)
return pv.MultiBlock([point_wheel_center])
[docs]
def spring(point_a, point_b, radius=5):
"""
Generate a spring.
Parameters
----------
point_a : array-like
Coordinates of the upper mounting point of the spring.
point_b : array-like
Coordinates of the lower mounting point of the spring.
radius : float, optional
Radius of the Spheres and the Tube, by default 10.
Returns
-------
pv.MultiBlock
MultiBlock containing two Spheres and a Tube representing the spring.
Notes
-----
The spring is formed by two Spheres at the upper and lower mounting points
and a Tube connecting them.
Examples
--------
Create a spring structure mapping between two coordinates and show it using PyVista.
>>> import numpy as np
>>> import pyvista as pv
>>> from pymycar.Cad import spring
>>> d = spring(np.array([831.7, -278.7, 251.2]), np.array([849.2, -419.1, 76.4]), radius=10)
>>> plotter = pv.Plotter()
>>> plotter.add_mesh(d, color='purple', name="Spring")
>>> plotter.add_title("Spring Structure")
>>> plotter.show()
"""
p1 = simple_sphere(point_a, radius)
p2 = simple_sphere(point_b, radius)
return pv.MultiBlock([p1, p2, simple_tube(point_a, point_b)])
[docs]
def simple_spring(point_a, point_b, radius=5, n_coils=10, spring_radius=15, n_points=500):
"""
Generate a spring with a defined number of coils spiraling around a line.
Parameters
----------
point_a : array-like
Coordinates of the upper mounting point of the spring.
point_b : array-like
Coordinates of the lower mounting point of the spring.
radius : float, optional
Radius of the wire Tube and mounting Spheres, by default 5.
n_coils : int, optional
Number of complete spiral coils, by default 10.
spring_radius : float, optional
The radius of the spiral itself, by default 15.
n_points : int, optional
The resolution (number of points) used to draw the spiral, by default 500.
Returns
-------
pv.MultiBlock
MultiBlock containing two Spheres and a spiral Tube representing the spring.
Notes
-----
The spring is formed by two Spheres at the upper and lower mounting points
and a spiral Tube generated as a parametric spline connecting them.
Examples
--------
Create a spring structure mapping between two coordinates and show it using PyVista.
>>> import numpy as np
>>> import pyvista as pv
>>> from pymycar.Cad.geometric_forms import simple_spring
>>> d = simple_spring(np.array([831.7, -278.7, 251.2]), np.array([849.2, -419.1, 76.4]), radius=5, n_coils=10, spring_radius=20)
>>> plotter = pv.Plotter()
>>> plotter.add_mesh(d, color='purple', name="Spring")
>>> plotter.add_title("Spring Structure")
>>> plotter.show()
"""
point_a = np.array(point_a, dtype=float).flatten()
point_b = np.array(point_b, dtype=float).flatten()
# 1) Calculate direction and length
direction = point_b - point_a
length = np.linalg.norm(direction)
if length == 0:
raise ValueError("point_a and point_b cannot be the same point (zero length direction).")
direction = direction / length
# 2) Parametric equations for a spiral along the Z-axis
t = np.linspace(0, 1, n_points)
x = spring_radius * np.cos(2 * np.pi * n_coils * t)
y = spring_radius * np.sin(2 * np.pi * n_coils * t)
z = length * t
points_z = np.column_stack((x, y, z))
# 3) Create a spline through the points and convert to a tube
spline = pv.Spline(points_z)
tube = spline.tube(radius=radius)
# 4) Calculate rotation from Z-axis to the desired direction
z_axis = np.array([0.0, 0.0, 1.0])
if np.allclose(direction, z_axis):
pass # No rotation necessary
elif np.allclose(direction, -z_axis):
tube.rotate_vector([1.0, 0.0, 0.0], 180.0, inplace=True)
else:
rot_axis = np.cross(z_axis, direction)
rot_angle = np.degrees(np.arccos(np.clip(np.dot(z_axis, direction), -1.0, 1.0)))
tube.rotate_vector(rot_axis, rot_angle, inplace=True)
# 5) Translate the tube to start at point_a
tube.translate(point_a, inplace=True)
# 6) Generate end spheres
p1 = simple_sphere(point_a, radius * 1.5)
p2 = simple_sphere(point_b, radius * 1.5)
return pv.MultiBlock([p1, p2, tube])
[docs]
def spring_old(point_a, point_b, radius=10, coil_radius=10, n_coils=1, n_points=1000):
"""
Generate a spring.
Parameters
----------
point_a : array-like
Coordinates of the upper mounting point of the spring.
point_b : array-like
Coordinates of the lower mounting point of the spring.
radius : float, optional
Radius of the Spheres, by default 10.
coil_radius : float, optional
Radius of the spring coil, by default 5.
n_coils : int, optional
Number of coils in the spring, by default 10.
n_points : int, optional
Number of points to represent the spring coil, by default 100.
Returns
-------
pv.MultiBlock
MultiBlock containing two Spheres and a Helix representing the spring.
Notes
-----
The spring is formed by two Spheres at the upper and lower mounting points
and a Helix representing the spring coil.
"""
# Create spheres at the mounting points
p1 = pv.Sphere(radius=radius, center=point_a)
p2 = pv.Sphere(radius=radius, center=point_b)
p3 = pv.Sphere(radius=radius, center=[0,0,0])
# Calculate the direction and length of the spring
direction = np.array(point_b) - np.array(point_a)
length = np.linalg.norm(direction)
if length == 0:
raise ValueError("point_a and point_b cannot be the same point (zero length direction).")
direction = direction.astype(float) / length
# Calculate a perpendicular direction
if np.allclose(direction, [0, 0, 1]):
perpendicular = np.array([1, 0, 0])
else:
reference_vector = np.array([0, 0, 1])
perpendicular = np.cross(direction, reference_vector)
perpendicular /= np.linalg.norm(perpendicular)
# Create a polygon to represent the cross-section of the spring coil
profile = pv.Polygon(
center = [0,0,0],
radius = coil_radius,
normal =perpendicular,
n_sides=30,
)
# Create the helical shape using extrude_rotate
angle = 360 * n_coils
extruded = profile.extrude_rotate(
resolution=n_points,
translation=length,
dradius=0.0,
angle=angle,
capping=True,
rotation_axis=direction
)
return pv.MultiBlock([p1, p2, p3, extruded])
[docs]
def rocked(pivot, point_a, point_b):
"""
Generate a structure with tubes connecting various points.
Parameters
----------
pivot : array-like
Coordinates of the central pivot point.
point_a : array-like
Coordinates of the first connecting point.
point_b : array-like
Coordinates of the second connecting point.
Returns
-------
pv.MultiBlock
MultiBlock containing three Tubes representing the structure.
Notes
-----
The structure is formed by three Tubes connecting various points.
"""
e1 = simple_tube(pivot, point_a)
e2 = simple_tube(pivot, point_b)
e3 = simple_tube(point_a, point_b)
return pv.MultiBlock([e1, e2, e3])
[docs]
def rectangle_U(base_a, base_b, point_a, point_b, radius=10, resolution=100, n_sides=10):
"""
Generate a U-shaped rectangular control arm using three tubes.
Parameters
----------
base_a : array-like
Coordinates of the first base point of the control arm.
base_b : array-like
Coordinates of the second base point of the control arm.
point_a : array-like
Coordinates of the extended point connecting to base_a.
point_b : array-like
Coordinates of the extended point connecting to base_b.
radius : float, optional
Radius of the tubes, by default 10.
resolution : int, optional
Resolution of the tubes, by default 100.
n_sides : int, optional
Number of sides of the tubes, by default 10.
Returns
-------
pv.MultiBlock
MultiBlock containing three tubes representing the U-shaped control arm.
Notes
-----
The control arm is formed by two tubes connecting the base points
to their respective extended points, and a third tube connecting the two extended points.
This forms a U-shaped rectangular structure.
"""
e1 = pv.Tube(pointa=base_a, pointb=point_a, resolution=resolution, radius=radius, n_sides=n_sides)
e2 = pv.Tube(pointa=base_b, pointb=point_b, resolution=resolution, radius=radius, n_sides=n_sides)
e3 = pv.Tube(pointa=point_a, pointb=point_b, resolution=resolution, radius=radius, n_sides=n_sides)
return pv.MultiBlock([e1, e2, e3])