Source code for yapcad.collision.result

"""Collision detection result data structures.

This module defines the CollisionResult dataclass and CollisionMethod enum
that represent the output of collision detection operations.

Copyright (c) 2026 yapCAD contributors
License: MIT
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum, auto
from typing import List, Tuple, Optional, Any, Dict


[docs] class CollisionMethod(Enum): """Method used for collision detection. Attributes: BREP: Exact BREP boolean intersection (pythonocc) MESH: Mesh-based sampling and containment (trimesh) AABB: Axis-aligned bounding box check only FCL: Flexible Collision Library (via trimesh) UNKNOWN: Method not specified or hybrid approach """ BREP = auto() MESH = auto() AABB = auto() FCL = auto() UNKNOWN = auto()
[docs] @dataclass class CollisionResult: """Result of collision detection between two parts. This dataclass captures all information about a collision check between two assembly parts, including whether they collide, the detection method used, collision metrics, and interface compatibility status. Attributes: part_a: Name/identifier of the first part part_b: Name/identifier of the second part collides: True if the parts geometrically intersect method: Detection method used (BREP, MESH, AABB, FCL) intersection_volume: Volume of intersection region in mm^3 (if computed) penetration_depth: Maximum penetration depth in mm (if computed) contact_points: List of contact/intersection points as (x, y, z) tuples compatible_interface: True if overlap is due to compatible interfaces (e.g., meshing gears with matching module/pressure angle) interface_names: Names of compatible interfaces that explain the overlap error_message: Error message if detection failed metadata: Additional method-specific data Example: >>> result = CollisionResult( ... part_a="SUN_GEAR", ... part_b="PLANET_GEAR_1", ... collides=True, ... method=CollisionMethod.BREP, ... intersection_volume=15.3, ... compatible_interface=True, ... interface_names=["sun_teeth", "planet_1_teeth"] ... ) >>> if result.collides and not result.compatible_interface: ... print(f"ERROR: Unintended collision between {result.part_a} and {result.part_b}") ... elif result.collides: ... print(f"OK: Expected overlap (gear mesh) between {result.part_a} and {result.part_b}") Notes: - `collides=True` with `compatible_interface=True` indicates expected overlap (e.g., gear teeth meshing, threads engaging) - `collides=True` with `compatible_interface=False` indicates an actual collision that needs to be resolved - `intersection_volume` and `penetration_depth` may be None/0 if not computed by the detection method used """ part_a: str part_b: str collides: bool method: CollisionMethod = CollisionMethod.UNKNOWN intersection_volume: Optional[float] = None penetration_depth: float = 0.0 contact_points: List[Tuple[float, float, float]] = field(default_factory=list) compatible_interface: bool = False interface_names: List[str] = field(default_factory=list) error_message: str = "" metadata: Dict[str, Any] = field(default_factory=dict) def __str__(self) -> str: """Human-readable string representation.""" if self.error_message: return f"[ERROR] {self.part_a} <-> {self.part_b}: {self.error_message}" if not self.collides: return f"[OK] {self.part_a} <-> {self.part_b}: No collision" if self.compatible_interface: ifaces = ", ".join(self.interface_names) if self.interface_names else "compatible" return f"[INTERFACE] {self.part_a} <-> {self.part_b}: Expected overlap ({ifaces})" details = [] if self.intersection_volume is not None and self.intersection_volume > 0: details.append(f"volume={self.intersection_volume:.3f}mm^3") if self.penetration_depth > 0: details.append(f"depth={self.penetration_depth:.3f}mm") if self.contact_points: details.append(f"contacts={len(self.contact_points)}") detail_str = ", ".join(details) if details else "detected" return f"[COLLISION] {self.part_a} <-> {self.part_b}: {detail_str}" def __repr__(self) -> str: """Detailed repr for debugging.""" return ( f"CollisionResult(part_a={self.part_a!r}, part_b={self.part_b!r}, " f"collides={self.collides}, method={self.method.name}, " f"intersection_volume={self.intersection_volume}, " f"penetration_depth={self.penetration_depth}, " f"compatible_interface={self.compatible_interface})" ) @property def is_error(self) -> bool: """True if collision represents an actual error (not expected interface).""" return self.collides and not self.compatible_interface @property def is_interface_overlap(self) -> bool: """True if collision is due to compatible interface (expected overlap).""" return self.collides and self.compatible_interface @property def pair_key(self) -> Tuple[str, str]: """Canonical pair key (sorted alphabetically) for deduplication.""" return tuple(sorted([self.part_a, self.part_b]))
[docs] def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for JSON serialization. Returns: Dictionary representation suitable for JSON export """ return { "part_a": self.part_a, "part_b": self.part_b, "collides": self.collides, "method": self.method.name, "intersection_volume": self.intersection_volume, "penetration_depth": self.penetration_depth, "contact_points": self.contact_points, "compatible_interface": self.compatible_interface, "interface_names": self.interface_names, "error_message": self.error_message, "metadata": self.metadata, }
[docs] @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'CollisionResult': """Create CollisionResult from dictionary. Args: data: Dictionary with collision result data Returns: CollisionResult instance """ method_name = data.get("method", "UNKNOWN") try: method = CollisionMethod[method_name] except KeyError: method = CollisionMethod.UNKNOWN return cls( part_a=data["part_a"], part_b=data["part_b"], collides=data["collides"], method=method, intersection_volume=data.get("intersection_volume"), penetration_depth=data.get("penetration_depth", 0.0), contact_points=[tuple(p) for p in data.get("contact_points", [])], compatible_interface=data.get("compatible_interface", False), interface_names=data.get("interface_names", []), error_message=data.get("error_message", ""), metadata=data.get("metadata", {}), )
[docs] @classmethod def no_collision(cls, part_a: str, part_b: str, method: CollisionMethod = CollisionMethod.UNKNOWN) -> 'CollisionResult': """Factory for creating a no-collision result. Args: part_a: First part name part_b: Second part name method: Detection method used Returns: CollisionResult with collides=False """ return cls( part_a=part_a, part_b=part_b, collides=False, method=method, )
[docs] @classmethod def error(cls, part_a: str, part_b: str, message: str) -> 'CollisionResult': """Factory for creating an error result. Args: part_a: First part name part_b: Second part name message: Error message describing what went wrong Returns: CollisionResult with error_message set """ return cls( part_a=part_a, part_b=part_b, collides=False, error_message=message, )