""" GLB 3D model builder for roller coaster rails. Builds a tube mesh along each rail path using pure numpy + trimesh (no extra deps). build123d is installed for future complex solid geometry work. """ import logging import numpy as np logger = logging.getLogger(__name__) def _tube_mesh(path: np.ndarray, radius: float, segments: int = 8): """ Create a trimesh.Trimesh tube swept along a 3-D polyline path. Parameters ---------- path : (n, 3) float path centreline in metres radius : float tube radius in metres segments : int number of cross-section vertices """ import trimesh # type: ignore n = len(path) angles = np.linspace(0, 2 * np.pi, segments, endpoint=False) circle_xy = np.column_stack([np.cos(angles), np.sin(angles)]) # (seg, 2) rings = [] for i in range(n): # Tangent vector if i == 0: t = path[1] - path[0] elif i == n - 1: t = path[-1] - path[-2] else: t = path[i + 1] - path[i - 1] t = t / (np.linalg.norm(t) + 1e-12) # Build an orthonormal frame (right, up2) perpendicular to t world_up = np.array([0.0, 0.0, 1.0]) if abs(np.dot(t, world_up)) > 0.9: world_up = np.array([1.0, 0.0, 0.0]) right = np.cross(t, world_up) right /= np.linalg.norm(right) + 1e-12 up2 = np.cross(right, t) # Place the circle ring at path[i] ring = path[i] + (circle_xy[:, 0:1] * right + circle_xy[:, 1:2] * up2) * radius rings.append(ring) verts = np.array(rings).reshape(-1, 3) # (n * segments, 3) # Stitch adjacent rings into quads (two triangles each) faces = [] for i in range(n - 1): for j in range(segments): j1 = (j + 1) % segments a = i * segments + j b = i * segments + j1 c = (i + 1) * segments + j d = (i + 1) * segments + j1 faces.append([a, b, d]) faces.append([a, d, c]) return trimesh.Trimesh(vertices=verts, faces=np.array(faces), process=False) def build_glb(rail_1_pts: np.ndarray, rail_2_pts: np.ndarray, radius_m: float = 0.05) -> bytes: """ Build a GLB model containing two solid rail tubes. Parameters ---------- rail_1_pts, rail_2_pts : np.ndarray shape (n, 3) metres, local ENU frame radius_m : float tube radius in metres Returns ------- bytes GLB binary data """ import trimesh # type: ignore logger.info("Building rail tubes (radius=%.3f m)…", radius_m) mesh_1 = _tube_mesh(rail_1_pts, radius_m) mesh_2 = _tube_mesh(rail_2_pts, radius_m) # Steel-grey colour for mesh in (mesh_1, mesh_2): mesh.visual.face_colors = [180, 185, 192, 255] scene = trimesh.Scene([mesh_1, mesh_2]) glb_bytes = scene.export(file_type="glb") logger.info("GLB size: %d bytes", len(glb_bytes)) return glb_bytes