Reputation: 7911
Using Python (3.10.14 at the time of writing), how could one build a 3D mesh object (which can be saved in either STL, PLY or GLB/GLTF format) using:
with those constraints:
?
We can consider the 3D trajectory as being composed of straight segments only (no curves). This means that two segments of the 3D axis meet at an angle, i.e. that the derivative at this point is not continuous. The resulting 3D mesh should not have holes at those locations. Therefore, the "3D join style" should be determined with a given cap style (e.g. as described here for 2 dimensions).
The 3D path is given as a numpy 3D array as follow:
import numpy as np
path = np.array([
[ 5.6, 10.1, 3.3],
[ 5.6, 12.4, 9.7],
[10.2, 27.7, 17.1],
[25.3, 34.5, 19.2],
[55. , 28.3, 18.9],
[80.3, 24.5, 15.4]
])
The 2D rectangular shape is given as a Shapely 2.0.3 Polygon feature:
from shapely.geometry import Polygon
polygon = Polygon([[0, 0],[1.2, 0], [1.2, 0.8], [0, 0.8], [0, 0]])
I'm currently giving Trimesh 4.2.3 (Numpy 1.26.4 being available) a try by using sweep_polygon
but without success because each time the rectangle shape has to change direction, it also rotates around an axis perpendicular to the plane defined by the two egdes meeting at that vertex where the direction changes, violating the second constraint here above.
import numpy as np
from shapely.geometry import Polygon
from trimesh.creation import sweep_polygon
polygon = Polygon([[0, 0],[1.2, 0], [1.2, 0.8], [0, 0.8], [0, 0]])
path = np.array([
[ 5.6, 10.1, 3.3],
[ 5.6, 12.4, 9.7],
[10.2, 27.7, 17.1],
[25.3, 34.5, 19.2],
[55. , 28.3, 18.9],
[80.3, 24.5, 15.4]
])
mesh = sweep_polygon(polygon, path)
In addition, the sweep_polygon
doc says:
Doesn’t handle sharp curvature well.
which is a little obscure.
Mesh rendered in meshlab. The shape's tilt is clearly visible as it rises to the right.
The final goal is to run that in a Docker container on a headless server.
Upvotes: 0
Views: 289
Reputation: 7911
A quick update; since April 8, 2024 where PR 2206 was merged, Trimesh can now handle the banking issue natively:
The "con" is now that the cross section is no more constant along the path as the two triangles defining each border of the "box" are not in the same plane...
Please also note that the doc for sweep_polygon
in trimesh 4.2.0 specified the following parameters:
Parameters
----------
polygon : shapely.geometry.Polygon
Profile to sweep along path
path : (n, 3) float
A path in 3D
angles : (n,) float
Optional rotation angle relative to prior vertex
at each vertex
**kwargs : dict
Passed to `triangulate_polygon`.
Returns
-------
mesh : trimesh.Trimesh
Geometry of result
whereas today, in trimesh 4.5.2, it tells:
Parameters
----------
polygon : shapely.geometry.Polygon
Profile to sweep along path
path : (n, 3) float
A path in 3D
angles : (n,) float
Optional rotation angle relative to prior vertex
at each vertex.
cap
If an open path is passed apply a cap to both ends.
connect
If a closed path is passed connect the sweep into
a single watertight mesh.
kwargs : dict
Passed to the mesh constructor.
**triangulation
Passed to `triangulate_polygon`, i.e. `engine='triangle'`
Returns
-------
mesh : trimesh.Trimesh
Geometry of result
Upvotes: 0
Reputation: 11522
Intersting! I have been working with this kind of problems for digital twinning of stores. Here is a code that I have been using, adapted a little to your can. I ran this in Jupyter Notebook, so if you're using a GUI, you'll probably need to do some extra work:
import numpy as np
from shapely.geometry import Polygon
import trimesh
path = np.array([
[5.6, 10.1, 3.3],
[5.6, 12.4, 9.7],
[10.2, 27.7, 17.1],
[25.3, 34.5, 19.2],
[55.0, 28.3, 18.9],
[80.3, 24.5, 15.4]
])
rect_width = 1.2
rect_height = 0.8
def generate_mesh_vertices(path, width, height):
vertices = []
for point in path:
vertices.append([point[0] - width / 2, point[1] - height / 2, point[2]])
vertices.append([point[0] + width / 2, point[1] - height / 2, point[2]])
vertices.append([point[0] + width / 2, point[1] + height / 2, point[2]])
vertices.append([point[0] - width / 2, point[1] + height / 2, point[2]])
return np.array(vertices)
def generate_faces_for_path(num_path_points):
faces = []
for i in range(num_path_points - 1):
base_index = i * 4
faces += [
[base_index, base_index + 4, base_index + 1],
[base_index + 1, base_index + 4, base_index + 5],
[base_index + 1, base_index + 5, base_index + 2],
[base_index + 2, base_index + 5, base_index + 6],
[base_index + 2, base_index + 6, base_index + 3],
[base_index + 3, base_index + 6, base_index + 7],
[base_index + 3, base_index + 7, base_index],
[base_index, base_index + 7, base_index + 4]
]
return np.array(faces)
vertices = generate_mesh_vertices(path, rect_width, rect_height)
faces = generate_faces_for_path(len(path))
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
mesh.export('path_mesh.stl')
import matplotlib.pyplot as plt
scene = trimesh.Scene(mesh)
scene.show()
which gives
You can zoom in and out, rotate etc. Stores have sharp edges, but I think i also works well with smooth ones, given that you provide the right data.
Upvotes: 1