forma-3d-review-api / src /geometry /face_extractor.py
lomit's picture
Sync from forma-3d-review@b6d4687f5d0f2e5303758c97095ea7e38e740723
182efca verified
"""B-Rep face extraction: solid shells vs free surfaces."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from OCP.BRep import BRep_Tool
from OCP.TopAbs import TopAbs_FACE, TopAbs_SHELL, TopAbs_SOLID
from OCP.TopExp import TopExp_Explorer
from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape
logger = logging.getLogger(__name__)
@dataclass
class FaceExtractionResult:
"""Result of face extraction from a shape."""
faces: list[TopoDS_Face]
is_solid: bool
num_solids: int
num_shells: int
num_faces: int
def _count_topology(shape: TopoDS_Shape, topo_type) -> int:
"""Count topology entities of a given type."""
count = 0
exp = TopExp_Explorer(shape, topo_type)
while exp.More():
count += 1
exp.Next()
return count
def _has_surface_geometry(face: TopoDS_Face) -> bool:
"""Check that a face has valid underlying surface geometry."""
try:
surface = BRep_Tool.Surface_s(face)
return surface is not None
except Exception:
return False
def extract_faces(shape: TopoDS_Shape) -> FaceExtractionResult:
"""Extract faces from a B-Rep shape.
For solids (CATIA): extracts outer shell faces.
For free surfaces (Alias): collects all faces directly.
Args:
shape: The TopoDS_Shape to extract faces from.
Returns:
FaceExtractionResult with the list of faces and metadata.
"""
num_solids = _count_topology(shape, TopAbs_SOLID)
num_shells = _count_topology(shape, TopAbs_SHELL)
is_solid = num_solids > 0
faces: list[TopoDS_Face] = []
seen_hashes: set[int] = set()
if is_solid:
logger.info(
"Solid geometry detected (%d solid(s), %d shell(s)). "
"Extracting outer shell faces.",
num_solids, num_shells,
)
solid_exp = TopExp_Explorer(shape, TopAbs_SOLID)
while solid_exp.More():
shell_exp = TopExp_Explorer(solid_exp.Current(), TopAbs_FACE)
while shell_exp.More():
face = TopoDS.Face_s(shell_exp.Current())
h = hash(face)
if h not in seen_hashes and _has_surface_geometry(face):
seen_hashes.add(h)
faces.append(face)
shell_exp.Next()
solid_exp.Next()
else:
logger.info(
"Free-surface geometry detected (%d shell(s)). "
"Collecting all faces.",
num_shells,
)
face_exp = TopExp_Explorer(shape, TopAbs_FACE)
while face_exp.More():
face = TopoDS.Face_s(face_exp.Current())
h = hash(face)
if h not in seen_hashes and _has_surface_geometry(face):
seen_hashes.add(h)
faces.append(face)
face_exp.Next()
num_faces = len(faces)
logger.info("Extracted %d unique faces (duplicates removed: %s).",
num_faces, "solid" if is_solid else "surface")
return FaceExtractionResult(
faces=faces,
is_solid=is_solid,
num_solids=num_solids,
num_shells=num_shells,
num_faces=num_faces,
)