- added renderer
- Added undo
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
"""Convert OCC BRep shapes to mesh files for render backends.
|
||||
|
||||
Outputs PLY files (preferred by Mitsuba) or STL files.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def occ_shape_to_ply(
|
||||
shape,
|
||||
output_path: Optional[str] = None,
|
||||
linear_deflection: float = 0.1,
|
||||
angular_deflection: float = 0.15,
|
||||
) -> str:
|
||||
"""Tessellate an OCC TopoDS_Shape and write as PLY.
|
||||
|
||||
Returns the path to the written PLY file.
|
||||
"""
|
||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopAbs import TopAbs_FACE
|
||||
from OCP.TopoDS import TopoDS
|
||||
from OCP.BRep import BRep_Tool
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
|
||||
# Tessellate
|
||||
tess = BRepMesh_IncrementalMesh(
|
||||
shape, linear_deflection, False, angular_deflection, True
|
||||
)
|
||||
tess.Perform()
|
||||
|
||||
# Extract triangulation from all faces
|
||||
all_vertices: List[List[float]] = []
|
||||
all_faces: List[List[int]] = []
|
||||
vertex_offset = 0
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while explorer.More():
|
||||
face = TopoDS.Face_s(explorer.Current())
|
||||
location = TopLoc_Location()
|
||||
triangulation = BRep_Tool.Triangulation_s(face, location)
|
||||
|
||||
if triangulation is None:
|
||||
explorer.Next()
|
||||
continue
|
||||
|
||||
# Transform
|
||||
trsf = location.Transformation()
|
||||
|
||||
# Extract vertices (apply location transform to positions)
|
||||
nb_nodes = triangulation.NbNodes()
|
||||
for i in range(1, nb_nodes + 1):
|
||||
node = triangulation.Node(i)
|
||||
pnt = node.Transformed(trsf)
|
||||
all_vertices.append([pnt.X(), pnt.Y(), pnt.Z()])
|
||||
|
||||
# Extract triangles
|
||||
nb_triangles = triangulation.NbTriangles()
|
||||
for i in range(1, nb_triangles + 1):
|
||||
tri = triangulation.Triangle(i)
|
||||
n1, n2, n3 = tri.Get()
|
||||
all_faces.append([
|
||||
n1 - 1 + vertex_offset,
|
||||
n2 - 1 + vertex_offset,
|
||||
n3 - 1 + vertex_offset,
|
||||
])
|
||||
|
||||
vertex_offset += nb_nodes
|
||||
explorer.Next()
|
||||
|
||||
if not all_vertices:
|
||||
raise ValueError("Tessellation produced no vertices")
|
||||
|
||||
vertices = np.array(all_vertices, dtype=np.float32)
|
||||
faces = np.array(all_faces, dtype=np.uint32)
|
||||
|
||||
logger.info(
|
||||
f"Tessellation: {len(vertices)} vertices, {len(faces)} triangles"
|
||||
)
|
||||
|
||||
# Compute outward-facing vertex normals from triangle geometry.
|
||||
# This ensures consistent lighting even when OCC triangulation winding
|
||||
# is inconsistent across faces (e.g. after location transforms).
|
||||
normals = _compute_outward_normals(vertices, faces, shape)
|
||||
|
||||
# Write PLY with normals
|
||||
if output_path is None:
|
||||
fd, output_path = tempfile.mkstemp(suffix=".ply", prefix="fluency_render_")
|
||||
os.close(fd)
|
||||
|
||||
_write_ply(output_path, vertices, faces, normals)
|
||||
logger.info(f"Wrote PLY: {output_path}")
|
||||
return output_path
|
||||
|
||||
|
||||
def _compute_outward_normals(
|
||||
vertices: np.ndarray,
|
||||
faces: np.ndarray,
|
||||
shape,
|
||||
) -> np.ndarray:
|
||||
"""Compute outward-facing vertex normals.
|
||||
|
||||
1. Compute per-face normals from cross product of triangle edges.
|
||||
2. Determine correct orientation by checking face normals against the
|
||||
shape centroid (outward = away from center).
|
||||
3. Flip triangles with inward normals before accumulating to vertices.
|
||||
4. Average and normalize per-vertex normals.
|
||||
"""
|
||||
n_verts = len(vertices)
|
||||
v_normals = np.zeros((n_verts, 3), dtype=np.float64)
|
||||
|
||||
# Compute shape centroid for outward direction reference.
|
||||
# Use OCC bounding box if available, otherwise fall back to vertex bounds.
|
||||
if shape is not None:
|
||||
from OCP.Bnd import Bnd_Box
|
||||
from OCP.BRepBndLib import BRepBndLib
|
||||
|
||||
bbox = Bnd_Box()
|
||||
BRepBndLib.Add_s(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
else:
|
||||
vmin = vertices.min(axis=0).astype(np.float64)
|
||||
vmax = vertices.max(axis=0).astype(np.float64)
|
||||
xmin, ymin, zmin = vmin
|
||||
xmax, ymax, zmax = vmax
|
||||
|
||||
centroid = np.array(
|
||||
[(xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
# Ensure faces is 2D (numpy creates (3,) for single-face meshes)
|
||||
if faces.ndim == 1:
|
||||
faces = faces.reshape(1, -1)
|
||||
|
||||
# Compute face normals from triangle geometry
|
||||
v0 = vertices[faces[:, 0]]
|
||||
v1 = vertices[faces[:, 1]]
|
||||
v2 = vertices[faces[:, 2]]
|
||||
|
||||
edge1 = v1 - v0
|
||||
edge2 = v2 - v0
|
||||
face_normals = np.cross(edge1, edge2)
|
||||
|
||||
# Triangle centroids to test direction from shape center
|
||||
tri_centers = (v0 + v1 + v2) / 3.0
|
||||
to_tri = tri_centers - centroid
|
||||
|
||||
# Dot product: positive means normal points away from centroid (outward)
|
||||
dots = np.sum(face_normals * to_tri, axis=1)
|
||||
|
||||
# Faces with negative dot have inward normals — swap columns 1 and 2
|
||||
flip_mask = dots < 0
|
||||
corrected_faces = faces.copy()
|
||||
col1 = corrected_faces[:, 1]
|
||||
col2 = corrected_faces[:, 2]
|
||||
corrected_faces[flip_mask, 1] = col2[flip_mask]
|
||||
corrected_faces[flip_mask, 2] = col1[flip_mask]
|
||||
|
||||
# Recompute face normals after correction
|
||||
v0c = vertices[corrected_faces[:, 0]]
|
||||
v1c = vertices[corrected_faces[:, 1]]
|
||||
v2c = vertices[corrected_faces[:, 2]]
|
||||
fn = np.cross(v1c - v0c, v2c - v0c)
|
||||
|
||||
# Normalize face normals
|
||||
lengths = np.linalg.norm(fn, axis=1, keepdims=True)
|
||||
lengths[lengths < 1e-10] = 1.0
|
||||
fn /= lengths
|
||||
|
||||
# Accumulate to vertices
|
||||
for i in range(len(corrected_faces)):
|
||||
idx = corrected_faces[i]
|
||||
v_normals[idx[0]] += fn[i]
|
||||
v_normals[idx[1]] += fn[i]
|
||||
v_normals[idx[2]] += fn[i]
|
||||
|
||||
# Normalize vertex normals
|
||||
v_lengths = np.linalg.norm(v_normals, axis=1, keepdims=True)
|
||||
v_lengths[v_lengths < 1e-10] = 1.0
|
||||
v_normals /= v_lengths
|
||||
|
||||
return v_normals.astype(np.float32)
|
||||
|
||||
|
||||
def occ_shape_to_stl(
|
||||
shape,
|
||||
output_path: Optional[str] = None,
|
||||
linear_deflection: float = 0.1,
|
||||
) -> str:
|
||||
"""Tessellate an OCC TopoDS_Shape and write as binary STL.
|
||||
|
||||
Returns the path to the written STL file.
|
||||
"""
|
||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCP.StlAPI import StlAPI_Writer
|
||||
|
||||
# Tessellate
|
||||
tess = BRepMesh_IncrementalMesh(shape, linear_deflection, False, 0.5, True)
|
||||
tess.Perform()
|
||||
|
||||
if output_path is None:
|
||||
fd, output_path = tempfile.mkstemp(suffix=".stl", prefix="fluency_render_")
|
||||
os.close(fd)
|
||||
|
||||
writer = StlAPI_Writer()
|
||||
writer.SetASCIIMode(False)
|
||||
writer.Write(shape, output_path)
|
||||
logger.info(f"Wrote STL: {output_path}")
|
||||
return output_path
|
||||
|
||||
|
||||
def occ_shape_bounds(shape) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]:
|
||||
"""Return (min_xyz, max_xyz) bounding box of an OCC shape."""
|
||||
from OCP.Bnd import Bnd_Box
|
||||
from OCP.BRepBndLib import BRepBndLib
|
||||
|
||||
bbox = Bnd_Box()
|
||||
BRepBndLib.Add_s(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
return (xmin, ymin, zmin), (xmax, ymax, zmax)
|
||||
|
||||
|
||||
def _write_ply(
|
||||
path: str,
|
||||
vertices: np.ndarray,
|
||||
faces: np.ndarray,
|
||||
normals: Optional[np.ndarray] = None,
|
||||
) -> None:
|
||||
"""Write a binary PLY file (little-endian) with optional vertex normals."""
|
||||
import struct
|
||||
|
||||
n_verts = len(vertices)
|
||||
n_faces = len(faces)
|
||||
has_normals = normals is not None and len(normals) == n_verts
|
||||
|
||||
with open(path, "wb") as f:
|
||||
# Header
|
||||
header_lines = [
|
||||
"ply",
|
||||
"format binary_little_endian 1.0",
|
||||
f"element vertex {n_verts}",
|
||||
"property float x",
|
||||
"property float y",
|
||||
"property float z",
|
||||
]
|
||||
if has_normals:
|
||||
header_lines.extend([
|
||||
"property float nx",
|
||||
"property float ny",
|
||||
"property float nz",
|
||||
])
|
||||
header_lines.append(f"element face {n_faces}")
|
||||
header_lines.append("property list uchar int vertex_indices")
|
||||
header_lines.append("end_header")
|
||||
|
||||
f.write(("\n".join(header_lines) + "\n").encode("ascii"))
|
||||
|
||||
# Vertex positions (+ normals if available)
|
||||
for i in range(n_verts):
|
||||
f.write(struct.pack("<fff", vertices[i, 0], vertices[i, 1], vertices[i, 2]))
|
||||
if has_normals:
|
||||
f.write(struct.pack("<fff", normals[i, 0], normals[i, 1], normals[i, 2]))
|
||||
|
||||
# Faces
|
||||
for face in faces:
|
||||
f.write(struct.pack("<B", 3))
|
||||
f.write(struct.pack("<iii", int(face[0]), int(face[1]), int(face[2])))
|
||||
Reference in New Issue
Block a user