Files
cdsl-cad/backend/vendor/cdsl_preview_runtime/step_to_glb.py
T
2026-08-19 19:34:30 +08:00

146 lines
6.0 KiB
Python

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("<fff", *vertex) for vertex in vertices)
normal_bytes = b"".join(struct.pack("<fff", *normal) for normal in normals)
indices = b"".join(struct.pack("<III", *triangle) for triangle in triangles)
position_offset = 0
normal_offset = len(positions)
index_offset = normal_offset + len(normal_bytes)
binary = _align_four(positions + normal_bytes + indices)
minimum = [min(vertex[index] for vertex in vertices) for index in range(3)]
maximum = [max(vertex[index] for vertex in vertices) for index in range(3)]
document = {
"asset": {"version": "2.0", "generator": "cdsl-cad-local-preview"},
"scene": 0,
"scenes": [{"nodes": [0]}],
"nodes": [{"mesh": 0, "name": step_path.stem}],
"meshes": [{"primitives": [{"attributes": {"POSITION": 0, "NORMAL": 1}, "indices": 2, "material": 0}]}],
"materials": [{
"name": "CDSL CAD",
"pbrMetallicRoughness": {
"baseColorFactor": [0.59, 0.67, 0.73, 1.0],
"metallicFactor": 0.22,
"roughnessFactor": 0.43,
},
}],
"buffers": [{"byteLength": len(binary)}],
"bufferViews": [
{"buffer": 0, "byteOffset": position_offset, "byteLength": len(positions), "target": 34962},
{"buffer": 0, "byteOffset": normal_offset, "byteLength": len(normal_bytes), "target": 34962},
{"buffer": 0, "byteOffset": index_offset, "byteLength": len(indices), "target": 34963},
],
"accessors": [
{"bufferView": 0, "componentType": 5126, "count": len(vertices), "type": "VEC3", "min": minimum, "max": maximum},
{"bufferView": 1, "componentType": 5126, "count": len(normals), "type": "VEC3"},
{"bufferView": 2, "componentType": 5125, "count": len(triangles) * 3, "type": "SCALAR"},
],
}
json_chunk = _align_four(json.dumps(document, separators=(",", ":")).encode("utf-8"), b" ")
glb = b"glTF" + struct.pack("<II", 2, 12 + 8 + len(json_chunk) + 8 + len(binary))
glb += struct.pack("<I4s", len(json_chunk), b"JSON") + json_chunk
glb += struct.pack("<I4s", len(binary), b"BIN\x00") + binary
glb_path.parent.mkdir(parents=True, exist_ok=True)
glb_path.write_bytes(glb)
return {
"vertices": len(vertices),
"triangles": len(triangles),
"bbox_min": minimum,
"bbox_max": maximum,
"topology_faces": topology_faces,
}