mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
191 lines
6.6 KiB
Python
191 lines
6.6 KiB
Python
import concurrent
|
|
import copyreg
|
|
from concurrent.futures import ProcessPoolExecutor, Executor
|
|
from dataclasses import dataclass
|
|
from typing import Tuple, Callable, Generator
|
|
|
|
import OCP
|
|
import numpy as np
|
|
from OCP.BRep import BRep_Tool
|
|
from OCP.BRepAdaptor import BRepAdaptor_Curve
|
|
from OCP.GCPnts import GCPnts_TangentialDeflection
|
|
from OCP.TopLoc import TopLoc_Location
|
|
from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex
|
|
from build123d import Face, Vector, Shape, Vertex
|
|
from partcad.wrappers import cq_serialize
|
|
from pygltflib import LINE_STRIP, GLTF2, Material, PbrMetallicRoughness, TRIANGLES, POINTS, TextureInfo
|
|
|
|
from gltf import create_gltf, _checkerboard_image
|
|
|
|
|
|
@dataclass
|
|
class TessellationUpdate:
|
|
"""Tessellation update"""
|
|
progress: float
|
|
"""Progress in percent"""
|
|
|
|
# Current shape
|
|
shape: TopoDS_Shape
|
|
"""(Sub)shape that was tessellated"""
|
|
gltf: GLTF2
|
|
"""The valid GLTF containing only the current shape"""
|
|
|
|
@property
|
|
def kind(self) -> str:
|
|
"""The kind of the shape"""
|
|
if isinstance(self.shape, TopoDS_Face):
|
|
return "face"
|
|
elif isinstance(self.shape, TopoDS_Edge):
|
|
return "edge"
|
|
elif isinstance(self.shape, TopoDS_Vertex):
|
|
return "vertex"
|
|
else:
|
|
raise ValueError(f"Unknown shape type: {self.shape}")
|
|
|
|
|
|
progress_callback_t = Callable[[TessellationUpdate], None]
|
|
|
|
|
|
def _inflate_vec(*values: float):
|
|
pnt = OCP.gp.gp_Vec(values[0], values[1], values[2])
|
|
return pnt
|
|
|
|
|
|
def _reduce_vec(pnt: OCP.gp.gp_Vec):
|
|
return _inflate_vec, (pnt.X(), pnt.Y(), pnt.Z())
|
|
|
|
|
|
def tessellate_count(ocp_shape: TopoDS_Shape) -> int:
|
|
"""Count the number of elements that will be tessellated"""
|
|
shape = Shape(ocp_shape)
|
|
return len(shape.faces()) + len(shape.edges()) + len(shape.vertices())
|
|
|
|
|
|
def tessellate(
|
|
ocp_shape: TopoDS_Shape,
|
|
tolerance: float = 0.1,
|
|
angular_tolerance: float = 0.1,
|
|
executor: Executor = ProcessPoolExecutor(), # Set to ThreadPoolExecutor if pickling fails...
|
|
) -> Generator[TessellationUpdate, None, None]:
|
|
"""Tessellate a whole shape into a list of triangle vertices and a list of triangle indices.
|
|
|
|
It uses multiprocessing to speed up the process, and publishes progress updates to the callback.
|
|
"""
|
|
shape = Shape(ocp_shape)
|
|
_register_pickle_if_needed()
|
|
with executor:
|
|
futures = []
|
|
|
|
# Submit tessellation tasks
|
|
for face in shape.faces():
|
|
futures.append(executor.submit(_tessellate_element, face.wrapped, tolerance, angular_tolerance))
|
|
for edge in shape.edges():
|
|
futures.append(executor.submit(_tessellate_element, edge.wrapped, tolerance, angular_tolerance))
|
|
for vertex in shape.vertices():
|
|
futures.append(executor.submit(_tessellate_element, vertex.wrapped, tolerance, angular_tolerance))
|
|
|
|
# Collect results as they come in
|
|
for i, future in enumerate(concurrent.futures.as_completed(futures)):
|
|
sub_shape, gltf = future.result()
|
|
yield TessellationUpdate(
|
|
progress=(i + 1) / len(futures),
|
|
shape=sub_shape,
|
|
gltf=gltf,
|
|
)
|
|
|
|
|
|
_pickle_registered = False
|
|
|
|
|
|
def _register_pickle_if_needed():
|
|
global _pickle_registered
|
|
if _pickle_registered:
|
|
return
|
|
cq_serialize.register()
|
|
copyreg.pickle(OCP.gp.gp_Vec, _reduce_vec)
|
|
|
|
|
|
# Define the function that will tessellate each element in parallel
|
|
def _tessellate_element(
|
|
element: TopoDS_Shape, tolerance: float, angular_tolerance: float) -> Tuple[TopoDS_Shape, GLTF2]:
|
|
if isinstance(element, TopoDS_Face):
|
|
return element, _tessellate_face(element, tolerance, angular_tolerance)
|
|
elif isinstance(element, TopoDS_Edge):
|
|
return element, _tessellate_edge(element, angular_tolerance, angular_tolerance)
|
|
elif isinstance(element, TopoDS_Vertex):
|
|
return element, _tessellate_vertex(element)
|
|
else:
|
|
raise ValueError(f"Unknown element type: {element}")
|
|
|
|
|
|
TriMesh = Tuple[list[Vector], list[Tuple[int, int, int]]]
|
|
|
|
|
|
def _tessellate_face(
|
|
ocp_face: TopoDS_Face,
|
|
tolerance: float = 0.1,
|
|
angular_tolerance: float = 0.1
|
|
) -> GLTF2:
|
|
"""Tessellate a face into a list of triangle vertices and a list of triangle indices"""
|
|
face = Face(ocp_face)
|
|
tri_mesh = face.tessellate(tolerance, angular_tolerance)
|
|
|
|
# Get UV of each face from the parameters
|
|
loc = TopLoc_Location()
|
|
poly = BRep_Tool.Triangulation_s(face.wrapped, loc)
|
|
uv = [
|
|
[v.X(), v.Y()]
|
|
for v in (poly.UVNode(i) for i in range(1, poly.NbNodes() + 1))
|
|
]
|
|
|
|
vertices = np.array(list(map(lambda v: [v.X, v.Y, v.Z], tri_mesh[0])))
|
|
indices = np.array(tri_mesh[1])
|
|
tex_coord = np.array(uv)
|
|
mode = TRIANGLES
|
|
material = Material(pbrMetallicRoughness=PbrMetallicRoughness(
|
|
baseColorFactor=[0.3, 1.0, 0.2, 1.0], roughnessFactor=0.1, baseColorTexture=TextureInfo(index=0)),
|
|
alphaCutoff=None)
|
|
return create_gltf(vertices, indices, tex_coord, mode, material, images=[_checkerboard_image])
|
|
|
|
|
|
def _tessellate_edge(
|
|
ocp_edge: TopoDS_Edge,
|
|
angular_deflection: float = 0.1,
|
|
curvature_deflection: float = 0.1,
|
|
) -> GLTF2:
|
|
"""Tessellate a wire or edge into a list of ordered vertices"""
|
|
curve = BRepAdaptor_Curve(ocp_edge)
|
|
discretizer = GCPnts_TangentialDeflection(curve, angular_deflection, curvature_deflection)
|
|
assert discretizer.NbPoints() > 1, "Edge is too small??"
|
|
|
|
# TODO: get and apply transformation??
|
|
|
|
# add vertices
|
|
vertices: list[list[float]] = [
|
|
[v.X(), v.Y(), v.Z()]
|
|
for v in (
|
|
discretizer.Value(i) # .Transformed(transformation)
|
|
for i in range(1, discretizer.NbPoints() + 1)
|
|
)
|
|
]
|
|
indices = np.array(list(map(lambda i: [i, i + 1], range(len(vertices) - 1))), dtype=np.uint8)
|
|
tex_coord = np.array([], dtype=np.float32)
|
|
mode = LINE_STRIP
|
|
material = Material(
|
|
pbrMetallicRoughness=PbrMetallicRoughness(baseColorFactor=[1.0, 1.0, 0.5, 1.0]),
|
|
alphaCutoff=None)
|
|
return create_gltf(np.array(vertices), indices, tex_coord, mode, material)
|
|
|
|
|
|
def _tessellate_vertex(ocp_vertex: TopoDS_Vertex) -> GLTF2:
|
|
"""Tessellate a vertex into a list of triangle vertices and a list of triangle indices"""
|
|
c = Vertex(ocp_vertex).center()
|
|
vertices = np.array([[c.X, c.Y, c.Z]])
|
|
indices = np.array([0])
|
|
tex_coord = np.array([], dtype=np.float32)
|
|
mode = POINTS
|
|
material = Material(
|
|
pbrMetallicRoughness=PbrMetallicRoughness(baseColorFactor=[1.0, 0.5, 0.5, 1.0]),
|
|
alphaCutoff=None)
|
|
return create_gltf(vertices, indices, tex_coord, mode, material)
|