- Basic 2D projection

This commit is contained in:
bklronin
2024-07-08 22:14:25 +02:00
parent 9daf263aad
commit 5ff48c0f5e
16 changed files with 967 additions and 826 deletions

View File

@@ -0,0 +1,44 @@
# Draw simple boundary based on the lines and depth
def generate_mesh(lines: list, z_origin: float, depth: float, symmetric: bool = True):
if symmetric:
depth1 = depth/2
depth2 = -depth/2
origin = create_3D(lines, z_origin, depth1)
extruded = create_3D(lines, z_origin, depth2)
else:
origin = create_3D(lines, z_origin, 0)
extruded = create_3D(lines, z_origin, depth)
vert_lines = create_vert_lines(origin, extruded)
print(f"Result = {origin} / {extruded} / {vert_lines}")
return origin + vert_lines + extruded
def create_vert_lines(origin, extruded):
vert_lines = []
for d3_point_o, d3point_e in zip(origin, extruded):
for sp3d_1, sp3d_2 in zip(d3_point_o, d3point_e):
new_line = sp3d_1, sp3d_2
vert_lines.append(new_line)
return vert_lines
def create_3D(lines, z_origin, depth):
line_loop = []
for coordinate2d in lines:
start, end = coordinate2d
xs,ys = start
coordinate3d_start_orig = xs, ys, z_origin + depth
xe, ye = end
coordinate3d_end_orig = xe, ye, z_origin + depth
line3d_orig = coordinate3d_start_orig, coordinate3d_end_orig
line_loop.append(line3d_orig)
return line_loop

View File

@@ -1,9 +1,172 @@
import numpy as np
from scipy.spatial import ConvexHull
from stl import mesh
from scipy.spatial import Delaunay, ConvexHull
from shapely.geometry import Polygon, Point
def generate_mesh(points, depth):
def alpha_shape(points, alpha):
"""
Compute the alpha shape (concave hull) of a set of points.
"""
def add_edge(edges, edge_points, points, i, j):
"""Add a line between the i-th and j-th points if not in the list already"""
if (i, j) in edges or (j, i) in edges:
return
edges.add((i, j))
edge_points.append(points[[i, j]])
tri = Delaunay(points)
edges = set()
edge_points = []
# Loop over triangles:
for ia, ib, ic in tri.simplices:
pa = points[ia]
pb = points[ib]
pc = points[ic]
# Lengths of sides of triangle
a = np.sqrt((pa[0] - pb[0]) ** 2 + (pa[1] - pb[1]) ** 2)
b = np.sqrt((pb[0] - pc[0]) ** 2 + (pb[1] - pc[1]) ** 2)
c = np.sqrt((pc[0] - pa[0]) ** 2 + (pc[1] - pa[1]) ** 2)
# Semiperimeter of triangle
s = (a + b + c) / 2.0
# Area of triangle by Heron's formula
area = np.sqrt(s * (s - a) * (s - b) * (s - c))
circum_r = a * b * c / (4.0 * area)
# Here's the radius filter.
if circum_r < 1.0 / alpha:
add_edge(edges, edge_points, points, ia, ib)
add_edge(edges, edge_points, points, ib, ic)
add_edge(edges, edge_points, points, ic, ia)
m = np.array(edge_points)
return m
def generate_mesh(points, depth, alpha=0.1):
"""
Generate a mesh by extruding a 2D shape along the Z-axis, automatically detecting holes.
:param points: List of (x, y) tuples representing all points of the 2D shape, including potential holes.
:param depth: Extrusion depth along the Z-axis.
:param alpha: Alpha value for the alpha shape algorithm (controls the "tightness" of the boundary).
:return: Tuple of vertices and faces.
"""
# Convert points to a numpy array
points_2d = np.array(points)
# Compute the alpha shape (outer boundary)
boundary_edges = alpha_shape(points_2d, alpha)
# Create a Polygon from the boundary
boundary_polygon = Polygon(boundary_edges)
# Separate points into boundary and interior
boundary_points = []
interior_points = []
for point in points:
if Point(point).touches(boundary_polygon) or Point(point).within(boundary_polygon):
if Point(point).touches(boundary_polygon):
boundary_points.append(point)
else:
interior_points.append(point)
# Perform Delaunay triangulation on all points
tri = Delaunay(points_2d)
# Generate the top and bottom faces
bottom_face = np.hstack((tri.points, np.zeros((tri.points.shape[0], 1))))
top_face = np.hstack((tri.points, np.ones((tri.points.shape[0], 1)) * depth))
# Combine top and bottom vertices
vertices_array = np.vstack((bottom_face, top_face))
# Create faces
faces = []
# Bottom face triangulation
for simplex in tri.simplices:
faces.append(simplex.tolist())
# Top face triangulation (with an offset)
top_offset = len(tri.points)
for simplex in tri.simplices:
faces.append([i + top_offset for i in simplex])
# Side faces for the outer boundary
for i in range(len(boundary_points)):
next_i = (i + 1) % len(boundary_points)
current = points.index(boundary_points[i])
next_point = points.index(boundary_points[next_i])
faces.append([current, top_offset + current, top_offset + next_point])
faces.append([current, top_offset + next_point, next_point])
# Convert vertices to the desired format: list of tuples
vertices = [tuple(vertex) for vertex in vertices_array]
return vertices, faces
def generate_mesh_wholes(points, holes, depth):
"""
Generate a mesh by extruding a 2D shape along the Z-axis, including holes.
:param points: List of (x, y) tuples representing the outer boundary of the 2D shape.
:param holes: List of lists, where each inner list contains (x, y) tuples representing a hole.
:param depth: Extrusion depth along the Z-axis.
:return: Tuple of vertices and faces.
"""
# Convert points to a numpy array
points_2d = np.array(points)
# Prepare points for triangulation
triangulation_points = points_2d.tolist()
for hole in holes:
triangulation_points.extend(hole)
# Perform Delaunay triangulation
tri = Delaunay(np.array(triangulation_points))
# Generate the top and bottom faces
bottom_face = np.hstack((tri.points, np.zeros((tri.points.shape[0], 1))))
top_face = np.hstack((tri.points, np.ones((tri.points.shape[0], 1)) * depth))
# Combine top and bottom vertices
vertices_array = np.vstack((bottom_face, top_face))
# Create faces
faces = []
# Bottom face triangulation
for simplex in tri.simplices:
faces.append(simplex.tolist())
# Top face triangulation (with an offset)
top_offset = len(tri.points)
for simplex in tri.simplices:
faces.append([i + top_offset for i in simplex])
# Side faces
for i in range(len(points)):
next_i = (i + 1) % len(points)
faces.append([i, top_offset + i, top_offset + next_i])
faces.append([i, top_offset + next_i, next_i])
# Side faces for holes
start_index = len(points)
for hole in holes:
for i in range(len(hole)):
current = start_index + i
next_i = start_index + (i + 1) % len(hole)
faces.append([current, top_offset + next_i, top_offset + current])
faces.append([current, next_i, top_offset + next_i])
start_index += len(hole)
# Convert vertices to the desired format: list of tuples
vertices = [tuple(vertex) for vertex in vertices_array]
return vertices, faces
def generate_mesh_simple(points, depth):
"""
Generate a mesh by extruding a 2D shape along the Z-axis.

119
mesh_modules/vesta_mesh.py Normal file
View File

@@ -0,0 +1,119 @@
import numpy as np
from skimage import measure
import multiprocessing
from functools import partial
from multiprocessing.pool import ThreadPool
import itertools
import time
def _cartesian_product(*arrays):
la = len(arrays)
dtype = np.result_type(*arrays)
arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype)
for i, a in enumerate(np.ix_(*arrays)):
arr[..., i] = a
return arr.reshape(-1, la)
class VESTA:
def __init__(self, sdf, bounds=None, resolution=64, threshold=0.0, workers=None):
self.sdf = sdf
self.bounds = bounds
self.resolution = resolution
self.threshold = threshold
self.workers = workers or multiprocessing.cpu_count()
def _estimate_bounds(self):
s = 16
x0 = y0 = z0 = -1e9
x1 = y1 = z1 = 1e9
prev = None
for i in range(32):
X = np.linspace(x0, x1, s)
Y = np.linspace(y0, y1, s)
Z = np.linspace(z0, z1, s)
d = np.array([X[1] - X[0], Y[1] - Y[0], Z[1] - Z[0]])
threshold = np.linalg.norm(d) / 2
if threshold == prev:
break
prev = threshold
P = _cartesian_product(X, Y, Z)
volume = self.sdf(P).reshape((len(X), len(Y), len(Z)))
where = np.argwhere(np.abs(volume) <= threshold)
if where.size == 0:
continue
x1, y1, z1 = (x0, y0, z0) + where.max(axis=0) * d + d / 2
x0, y0, z0 = (x0, y0, z0) + where.min(axis=0) * d - d / 2
if prev is None:
raise ValueError("Failed to estimate bounds. No points found within any threshold.")
return ((x0, y0, z0), (x1, y1, z1))
def _vesta_worker(self, chunk):
x0, x1, y0, y1, z0, z1 = chunk
X = np.linspace(x0, x1, self.resolution)
Y = np.linspace(y0, y1, self.resolution)
Z = np.linspace(z0, z1, self.resolution)
P = _cartesian_product(X, Y, Z)
V = self.sdf(P).reshape((self.resolution, self.resolution, self.resolution))
try:
verts, faces, _, _ = measure.marching_cubes(V, self.threshold)
except RuntimeError:
# Return empty arrays if marching_cubes fails
return np.array([]), np.array([])
# Scale and translate vertices to match the chunk's bounds
verts = verts / (self.resolution - 1)
verts[:, 0] = verts[:, 0] * (x1 - x0) + x0
verts[:, 1] = verts[:, 1] * (y1 - y0) + y0
verts[:, 2] = verts[:, 2] * (z1 - z0) + z0
return verts, faces
def _merge_meshes(self, results):
all_verts = []
all_faces = []
offset = 0
for verts, faces in results:
if len(verts) > 0 and len(faces) > 0:
all_verts.append(verts)
all_faces.append(faces + offset)
offset += len(verts)
if not all_verts or not all_faces:
return np.array([]), np.array([])
return np.vstack(all_verts), np.vstack(all_faces)
def generate_mesh(self):
if self.bounds is None:
self.bounds = self._estimate_bounds()
(x0, y0, z0), (x1, y1, z1) = self.bounds
chunks = [
(x0, x1, y0, y1, z0, z1)
]
with ThreadPool(self.workers) as pool:
results = pool.map(self._vesta_worker, chunks)
verts, faces = self._merge_meshes(results)
return verts, faces
def generate_mesh_from_sdf(sdf, bounds=None, resolution=64, threshold=0.0, workers=None):
vesta = VESTA(sdf, bounds, resolution, threshold, workers)
return vesta.generate_mesh()
# Helper function to save the mesh as an STL file
def save_mesh_as_stl(vertices, faces, filename):
from stl import mesh
# Create the mesh
cube = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))
for i, f in enumerate(faces):
for j in range(3):
cube.vectors[i][j] = vertices[f[j], :]
# Write the mesh to file
cube.save(filename)