146 lines
6.0 KiB
Python
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,
|
|
}
|