from __future__ import annotations import json import math import struct from pathlib import Path from build123d import import_step def _align_four(data: bytes, fill: bytes = b"\x00") -> bytes: return data + fill * ((4 - len(data) % 4) % 4) def _normals( vertices: list[tuple[float, float, float]], triangles: list[tuple[int, int, int]], ) -> list[tuple[float, float, float]]: values = [[0.0, 0.0, 0.0] for _ in vertices] for a, b, c in triangles: ax, ay, az = vertices[a] bx, by, bz = vertices[b] cx, cy, cz = vertices[c] ux, uy, uz = bx - ax, by - ay, bz - az vx, vy, vz = cx - ax, cy - ay, cz - az nx, ny, nz = uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx for index in (a, b, c): values[index][0] += nx values[index][1] += ny values[index][2] += nz output: list[tuple[float, float, float]] = [] for x, y, z in values: length = math.sqrt(x * x + y * y + z * z) or 1.0 output.append((x / length, y / length, z / length)) return output def _vector(value: object) -> list[float]: return [float(value.X), float(value.Y), float(value.Z)] def _normalised(values: list[float]) -> list[float]: length = math.sqrt(sum(value * value for value in values)) or 1.0 return [value / length for value in values] def _face_frame(face: object) -> dict[str, object]: center = _vector(face.center()) normal = _normalised(_vector(face.normal_at())) candidate = [1.0, 0.0, 0.0] if abs(normal[0]) < 0.9 else [0.0, 1.0, 0.0] projection = sum(candidate[index] * normal[index] for index in range(3)) x_dir = _normalised([candidate[index] - projection * normal[index] for index in range(3)]) y_dir = [ normal[1] * x_dir[2] - normal[2] * x_dir[1], normal[2] * x_dir[0] - normal[0] * x_dir[2], normal[0] * x_dir[1] - normal[1] * x_dir[0], ] bbox = face.bounding_box() return { "center": center, "normal": normal, "surface_type": str(getattr(face, "geom_type", "UNKNOWN")).split(".")[-1].lower(), "frame": {"origin_mm": center, "normal": normal, "x_dir": x_dir, "y_dir": y_dir}, "bbox": { "min": [float(bbox.min.X), float(bbox.min.Y), float(bbox.min.Z)], "max": [float(bbox.max.X), float(bbox.max.Y), float(bbox.max.Z)], }, } def step_to_glb(step_path: Path, glb_path: Path, tolerance: float = 0.15) -> dict[str, object]: shape = import_step(str(step_path)) vertices: list[tuple[float, float, float]] = [] triangles: list[tuple[int, int, int]] = [] topology_faces: list[dict[str, object]] = [] for face_index, face in enumerate(shape.faces()): vectors, raw_triangles = face.tessellate(tolerance) if not vectors or not raw_triangles: continue vertex_offset = len(vertices) triangle_start = len(triangles) vertices.extend((float(vector.X), float(vector.Y), float(vector.Z)) for vector in vectors) triangles.extend( (vertex_offset + int(a), vertex_offset + int(b), vertex_offset + int(c)) for a, b, c in raw_triangles ) topology_faces.append({ "id": f"face_{face_index:03d}", "triangle_start": triangle_start, "triangle_count": len(raw_triangles), **_face_frame(face), }) if not vertices or not triangles: raise RuntimeError("STEP tessellation produced no renderable triangles") normals = _normals(vertices, triangles) positions = b"".join(struct.pack("