cadforge / server /geometry.py
eventhorizon28's picture
Upload folder using huggingface_hub
7c72eb2 verified
import logging
import time
from collections import Counter
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
logger = logging.getLogger(__name__)
def extract_properties(shape) -> Dict[str, Any]:
t0 = time.time()
try:
props = {}
bb = shape.BoundingBox()
dims = [bb.xlen, bb.ylen, bb.zlen]
sorted_axes = sorted(
zip(["X", "Y", "Z"], dims), key=lambda x: x[1], reverse=True
)
longest_axis = sorted_axes[0][0]
props["is_valid"] = shape.isValid()
volume = shape.Volume()
surface_area = shape.Area()
props["volume_mm3"] = round(volume, 4)
props["surface_area_mm2"] = round(surface_area, 4)
props["bbox_x_mm"] = round(bb.xlen, 4)
props["bbox_y_mm"] = round(bb.ylen, 4)
props["bbox_z_mm"] = round(bb.zlen, 4)
props["bbox_longest_axis"] = longest_axis
max_dim = max(dims)
props["bbox_ratio_yx"] = round(sorted_axes[1][1] / max_dim, 4) if max_dim > 0 else 0.0
props["bbox_ratio_zx"] = round(sorted_axes[2][1] / max_dim, 4) if max_dim > 0 else 0.0
faces = shape.Faces()
face_types = [f.geomType() for f in faces]
type_counts = dict(Counter(face_types))
total_faces = len(face_types)
props["face_count"] = total_faces
props["face_type_counts"] = type_counts
props["dominant_face_type"] = max(type_counts, key=type_counts.get) if type_counts else "UNKNOWN"
props["face_type_distribution"] = {
k: round(v / total_faces, 4) for k, v in type_counts.items()
} if total_faces > 0 else {}
props["edge_count"] = len(shape.Edges())
props["vertex_count"] = len(shape.Vertices())
V = props["vertex_count"]
E = props["edge_count"]
F = props["face_count"]
props["euler_characteristic"] = V - E + F
props["is_watertight"] = _check_watertight(shape)
xy_sym, xz_sym, yz_sym = _check_symmetry(shape, bb)
props["has_xy_symmetry"] = xy_sym
props["has_xz_symmetry"] = xz_sym
props["has_yz_symmetry"] = yz_sym
props["shape_class"] = _classify_shape(props)
elapsed = time.time() - t0
logger.info(f"extract_properties took {elapsed:.3f}s")
return props
except Exception as e:
elapsed = time.time() - t0
logger.error(f"extract_properties failed after {elapsed:.3f}s: {e}")
raise
def _check_watertight(shape) -> bool:
try:
shells = shape.Shells()
if not shells:
return False
for shell in shells:
if not shell.Closed():
return False
return True
except Exception:
return False
def _check_symmetry(shape, bb, n_sample: int = 200, tol_ratio: float = 0.05) -> Tuple[bool, bool, bool]:
try:
from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.BRep import BRep_Tool
from OCP.TopExp import TopExp_Explorer
from OCP.TopAbs import TopAbs_FACE
from OCP.TopLoc import TopLoc_Location
from OCP.TopoDS import TopoDS
mesh = BRepMesh_IncrementalMesh(shape.wrapped, 0.5, False, 0.5, True)
mesh.Perform()
points = []
explorer = TopExp_Explorer(shape.wrapped, TopAbs_FACE)
while explorer.More():
face = TopoDS.Face_s(explorer.Current())
loc = TopLoc_Location()
tri = BRep_Tool.Triangulation_s(face, loc)
if tri is not None:
for i in range(1, tri.NbNodes() + 1):
p = tri.Node(i)
trsf = loc.Transformation()
p.Transform(trsf)
points.append([p.X(), p.Y(), p.Z()])
explorer.Next()
if len(points) < 10:
return False, False, False
pts = np.array(points)
if len(pts) > n_sample:
idx = np.random.choice(len(pts), n_sample, replace=False)
pts = pts[idx]
diag = np.sqrt(bb.xlen**2 + bb.ylen**2 + bb.zlen**2)
tol = diag * tol_ratio
from scipy.spatial import cKDTree
tree = cKDTree(pts)
def check_plane_symmetry(pts_arr, axis_idx):
reflected = pts_arr.copy()
reflected[:, axis_idx] = -reflected[:, axis_idx]
dists, _ = tree.query(reflected)
return float(np.mean(dists)) < tol
cx = (bb.xmin + bb.xmax) / 2
cy = (bb.ymin + bb.ymax) / 2
cz = (bb.zmin + bb.zmax) / 2
centered = pts - np.array([cx, cy, cz])
tree_c = cKDTree(centered)
def check_sym(axis_idx):
reflected = centered.copy()
reflected[:, axis_idx] = -reflected[:, axis_idx]
dists, _ = tree_c.query(reflected)
return float(np.mean(dists)) < tol
has_yz = check_sym(0)
has_xz = check_sym(1)
has_xy = check_sym(2)
return has_xy, has_xz, has_yz
except Exception as e:
logger.warning(f"Symmetry check failed: {e}")
return False, False, False
def _classify_shape(props: Dict[str, Any]) -> str:
ratio_yx = props.get("bbox_ratio_yx", 0)
ratio_zx = props.get("bbox_ratio_zx", 0)
dominant = props.get("dominant_face_type", "")
euler = props.get("euler_characteristic", 2)
is_valid = props.get("is_valid", False)
if not is_valid or props.get("volume_mm3", 0) <= 0:
return "DEGENERATE"
is_flat = ratio_zx < 0.25
is_cubic = ratio_yx > 0.7 and ratio_zx > 0.7
is_round = dominant in ("CYLINDER", "CONE", "SPHERE", "TORUS")
is_tall = ratio_zx > 0.5 and not is_cubic
if is_round:
if is_flat:
return "FLAT_ROUND_SOLID"
if euler != 2:
return "HOLLOW_ROUND_SOLID"
if is_tall:
return "TALL_ROUND_SOLID"
return "FLAT_ROUND_SOLID"
if is_flat:
if euler != 2:
return "FLAT_SOLID_WITH_HOLES"
return "FLAT_SOLID"
if is_cubic:
if euler != 2:
return "CUBIC_SOLID_WITH_HOLES"
return "CUBIC_SOLID"
face_count = props.get("face_count", 0)
if face_count > 8 and ratio_yx < 0.5:
return "L_SHAPED_SOLID"
if face_count > 8 and ratio_yx > 0.5:
return "T_SHAPED_SOLID"
if face_count > 10:
return "STEPPED_SOLID"
return "COMPLEX_SOLID"