|
3 | 3 | from __future__ import annotations
|
4 | 4 |
|
5 | 5 | from abc import ABC
|
6 |
| -from typing import List, Optional, Tuple, Union |
| 6 | +from typing import Callable, List, Literal, Optional, Tuple, Union |
7 | 7 |
|
8 | 8 | import numpy as np
|
9 | 9 | import pydantic.v1 as pydantic
|
10 | 10 |
|
11 |
| -from ...constants import inf |
| 11 | +from ...constants import fp_eps, inf |
12 | 12 | from ...exceptions import DataError, ValidationError
|
13 | 13 | from ...log import log
|
14 | 14 | from ...packaging import verify_packages_import
|
@@ -308,6 +308,185 @@ def _triangles_to_trimesh(
|
308 | 308 |
|
309 | 309 | return trimesh.Trimesh(**trimesh.triangles.to_kwargs(triangles))
|
310 | 310 |
|
| 311 | + @classmethod |
| 312 | + def from_height_grid( |
| 313 | + cls, |
| 314 | + axis: Ax, |
| 315 | + direction: Literal["-", "+"], |
| 316 | + base: float, |
| 317 | + grid: Tuple[np.ndarray, np.ndarray], |
| 318 | + height: np.ndarray, |
| 319 | + ) -> TriangleMesh: |
| 320 | + """Construct a TriangleMesh object from grid based height information. |
| 321 | +
|
| 322 | + Parameters |
| 323 | + ---------- |
| 324 | + axis : Ax |
| 325 | + Axis of extrusion. |
| 326 | + direction : Literal["-", "+"] |
| 327 | + Direction of extrusion. |
| 328 | + base : float |
| 329 | + Coordinate of the base surface along the geometry's axis. |
| 330 | + grid : Tuple[np.ndarray, np.ndarray] |
| 331 | + Tuple of two one-dimensional arrays representing the sampling grid (XY, YZ, or ZX |
| 332 | + corresponding to values of axis) |
| 333 | + height : np.ndarray |
| 334 | + Height values sampled on the given grid. Can be 1D (raveled) or 2D (matching grid mesh). |
| 335 | +
|
| 336 | + Returns |
| 337 | + ------- |
| 338 | + TriangleMesh |
| 339 | + The resulting TriangleMesh geometry object. |
| 340 | + """ |
| 341 | + |
| 342 | + x_coords = grid[0] |
| 343 | + y_coords = grid[1] |
| 344 | + |
| 345 | + nx = len(x_coords) |
| 346 | + ny = len(y_coords) |
| 347 | + nt = nx * ny |
| 348 | + |
| 349 | + x_mesh, y_mesh = np.meshgrid(x_coords, y_coords, indexing="ij") |
| 350 | + |
| 351 | + sign = 1 |
| 352 | + if direction == "-": |
| 353 | + sign = -1 |
| 354 | + |
| 355 | + flat_height = np.ravel(height) |
| 356 | + if flat_height.shape[0] != nt: |
| 357 | + raise ValueError( |
| 358 | + f"Shape of flattened height array {flat_height.shape} does not match " |
| 359 | + f"the number of grid points {nt}." |
| 360 | + ) |
| 361 | + |
| 362 | + if np.any(flat_height < 0): |
| 363 | + raise ValueError("All height values must be non-negative.") |
| 364 | + |
| 365 | + max_h = np.max(flat_height) |
| 366 | + min_h_clip = fp_eps * max_h |
| 367 | + flat_height = np.clip(flat_height, min_h_clip, inf) |
| 368 | + |
| 369 | + vertices_raw_list = [ |
| 370 | + [np.ravel(x_mesh), np.ravel(y_mesh), base + sign * flat_height], # Alpha surface |
| 371 | + [np.ravel(x_mesh), np.ravel(y_mesh), base * np.ones(nt)], |
| 372 | + ] |
| 373 | + |
| 374 | + if direction == "-": |
| 375 | + vertices_raw_list = vertices_raw_list[::-1] |
| 376 | + |
| 377 | + vertices = np.hstack(vertices_raw_list).T |
| 378 | + vertices = np.roll(vertices, shift=axis - 2, axis=1) |
| 379 | + |
| 380 | + q0 = (np.arange(nx - 1)[:, None] * ny + np.arange(ny - 1)[None, :]).ravel() |
| 381 | + q1 = (np.arange(1, nx)[:, None] * ny + np.arange(ny - 1)[None, :]).ravel() |
| 382 | + q2 = (np.arange(1, nx)[:, None] * ny + np.arange(1, ny)[None, :]).ravel() |
| 383 | + q3 = (np.arange(nx - 1)[:, None] * ny + np.arange(1, ny)[None, :]).ravel() |
| 384 | + |
| 385 | + q0_b = nt + q0 |
| 386 | + q1_b = nt + q1 |
| 387 | + q2_b = nt + q2 |
| 388 | + q3_b = nt + q3 |
| 389 | + |
| 390 | + top_quads = np.stack((q0, q1, q2, q3), axis=-1) |
| 391 | + bottom_quads = np.stack((q0_b, q3_b, q2_b, q1_b), axis=-1) |
| 392 | + |
| 393 | + s1_q0 = (0 * ny + np.arange(ny - 1)).ravel() |
| 394 | + s1_q1 = (0 * ny + np.arange(1, ny)).ravel() |
| 395 | + s1_q2 = (nt + 0 * ny + np.arange(1, ny)).ravel() |
| 396 | + s1_q3 = (nt + 0 * ny + np.arange(ny - 1)).ravel() |
| 397 | + side1_quads = np.stack((s1_q0, s1_q1, s1_q2, s1_q3), axis=-1) |
| 398 | + |
| 399 | + s2_q0 = ((nx - 1) * ny + np.arange(ny - 1)).ravel() |
| 400 | + s2_q1 = (nt + (nx - 1) * ny + np.arange(ny - 1)).ravel() |
| 401 | + s2_q2 = (nt + (nx - 1) * ny + np.arange(1, ny)).ravel() |
| 402 | + s2_q3 = ((nx - 1) * ny + np.arange(1, ny)).ravel() |
| 403 | + side2_quads = np.stack((s2_q0, s2_q1, s2_q2, s2_q3), axis=-1) |
| 404 | + |
| 405 | + s3_q0 = (np.arange(nx - 1) * ny + 0).ravel() |
| 406 | + s3_q1 = (nt + np.arange(nx - 1) * ny + 0).ravel() |
| 407 | + s3_q2 = (nt + np.arange(1, nx) * ny + 0).ravel() |
| 408 | + s3_q3 = (np.arange(1, nx) * ny + 0).ravel() |
| 409 | + side3_quads = np.stack((s3_q0, s3_q1, s3_q2, s3_q3), axis=-1) |
| 410 | + |
| 411 | + s4_q0 = (np.arange(nx - 1) * ny + ny - 1).ravel() |
| 412 | + s4_q1 = (np.arange(1, nx) * ny + ny - 1).ravel() |
| 413 | + s4_q2 = (nt + np.arange(1, nx) * ny + ny - 1).ravel() |
| 414 | + s4_q3 = (nt + np.arange(nx - 1) * ny + ny - 1).ravel() |
| 415 | + side4_quads = np.stack((s4_q0, s4_q1, s4_q2, s4_q3), axis=-1) |
| 416 | + |
| 417 | + all_quads = np.vstack( |
| 418 | + (top_quads, bottom_quads, side1_quads, side2_quads, side3_quads, side4_quads) |
| 419 | + ) |
| 420 | + |
| 421 | + triangles_list = [ |
| 422 | + np.stack((all_quads[:, 0], all_quads[:, 1], all_quads[:, 3]), axis=-1), |
| 423 | + np.stack((all_quads[:, 3], all_quads[:, 1], all_quads[:, 2]), axis=-1), |
| 424 | + ] |
| 425 | + tri_faces = np.vstack(triangles_list) |
| 426 | + |
| 427 | + return cls.from_vertices_faces(vertices=vertices, faces=tri_faces) |
| 428 | + |
| 429 | + @classmethod |
| 430 | + def from_height_function( |
| 431 | + cls, |
| 432 | + axis: Ax, |
| 433 | + direction: Literal["-", "+"], |
| 434 | + base: float, |
| 435 | + center: Tuple[float, float], |
| 436 | + size: Tuple[float, float], |
| 437 | + grid_size: Tuple[int, int], |
| 438 | + height_func: Callable[[np.ndarray, np.ndarray], np.ndarray], |
| 439 | + ) -> TriangleMesh: |
| 440 | + """Construct a TriangleMesh object from analytical expression of height function. |
| 441 | + The height function should be vectorized to accept 2D meshgrid arrays. |
| 442 | +
|
| 443 | + Parameters |
| 444 | + ---------- |
| 445 | + axis : Ax |
| 446 | + Axis of extrusion. |
| 447 | + direction : Literal["-", "+"] |
| 448 | + Direction of extrusion. |
| 449 | + base : float |
| 450 | + Coordinate of the base rectangle along the geometry's axis. |
| 451 | + center : Tuple[float, float] |
| 452 | + Center of the base rectangle in the plane perpendicular to the extrusion axis |
| 453 | + (XY, YZ, or ZX corresponding to values of axis). |
| 454 | + size : Tuple[float, float] |
| 455 | + Size of the base rectangle in the plane perpendicular to the extrusion axis |
| 456 | + (XY, YZ, or ZX corresponding to values of axis). |
| 457 | + grid_size : Tuple[int, int] |
| 458 | + Number of grid points for discretization of the base rectangle |
| 459 | + (XY, YZ, or ZX corresponding to values of axis). |
| 460 | + height_func : Callable[[np.ndarray, np.ndarray], np.ndarray] |
| 461 | + Vectorized function to compute height values from 2D meshgrid coordinate arrays. |
| 462 | + It should take two ndarrays (x_mesh, y_mesh) and return an ndarray of heights. |
| 463 | +
|
| 464 | + Returns |
| 465 | + ------- |
| 466 | + TriangleMesh |
| 467 | + The resulting TriangleMesh geometry object. |
| 468 | + """ |
| 469 | + x_lin = np.linspace(center[0] - 0.5 * size[0], center[0] + 0.5 * size[0], grid_size[0]) |
| 470 | + y_lin = np.linspace(center[1] - 0.5 * size[1], center[1] + 0.5 * size[1], grid_size[1]) |
| 471 | + |
| 472 | + x_mesh, y_mesh = np.meshgrid(x_lin, y_lin, indexing="ij") |
| 473 | + |
| 474 | + height_values = height_func(x_mesh, y_mesh) |
| 475 | + |
| 476 | + if not (isinstance(height_values, np.ndarray) and height_values.shape == x_mesh.shape): |
| 477 | + raise ValueError( |
| 478 | + f"The 'height_func' must return a NumPy array with shape {x_mesh.shape}, " |
| 479 | + f"but got shape {getattr(height_values, 'shape', type(height_values))}." |
| 480 | + ) |
| 481 | + |
| 482 | + return cls.from_height_grid( |
| 483 | + axis=axis, |
| 484 | + direction=direction, |
| 485 | + base=base, |
| 486 | + grid=(x_lin, y_lin), |
| 487 | + height=height_values, |
| 488 | + ) |
| 489 | + |
311 | 490 | @cached_property
|
312 | 491 | @verify_packages_import(["trimesh"])
|
313 | 492 | def trimesh(
|
|
0 commit comments