Source code for yapcad.collision.registry

"""Interface registry for managing allowed overlap regions.

This module defines the InterfaceRegistry class for tracking and
querying interface volumes across an assembly.

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

from __future__ import annotations

from typing import Dict, List, Optional, Tuple, Any

from .interface import InterfaceVolume, InterfaceType, CompatibilityResult

# Try to import numpy for overlap calculations
try:
    import numpy as np
    HAS_NUMPY = True
except ImportError:
    HAS_NUMPY = False
    np = None


[docs] class InterfaceRegistry: """Registry for managing interface volumes in an assembly. The registry tracks all interface volumes and provides lookup methods for the collision detector to find compatible interfaces when overlap is detected between parts. Key Features: - Register/unregister interface volumes by name - Query interfaces by part name or interface type - Check overlap compatibility between parts - Find all compatible interfaces for a given interface Usage: >>> registry = InterfaceRegistry() >>> >>> # Register gear interfaces >>> registry.register(GearMeshInterface( ... name="sun_teeth", part_name="SUN_GEAR", ... module=0.75, teeth=18, pressure_angle=20.0, face_width=10.0 ... )) >>> registry.register(GearMeshInterface( ... name="planet_teeth", part_name="PLANET_GEAR", ... module=0.75, teeth=36, pressure_angle=20.0, face_width=10.0 ... )) >>> >>> # During collision detection >>> is_compatible, results = registry.check_overlap_compatibility( ... "SUN_GEAR", "PLANET_GEAR" ... ) >>> if is_compatible: ... print("Overlap is expected (gear mesh)") Notes: - Interface names must be unique within the registry - Parts can have multiple interfaces (e.g., gear teeth + bearing seat) - The registry does not store geometry, only interface definitions """ def __init__(self): """Initialize an empty interface registry.""" self._interfaces: Dict[str, InterfaceVolume] = {} self._by_part: Dict[str, List[str]] = {} self._by_type: Dict[InterfaceType, List[str]] = {}
[docs] def register(self, interface: InterfaceVolume) -> None: """Register an interface volume. Args: interface: InterfaceVolume to register Raises: ValueError: If interface with same name already registered """ if interface.name in self._interfaces: raise ValueError(f"Interface '{interface.name}' already registered") self._interfaces[interface.name] = interface # Index by part if interface.part_name not in self._by_part: self._by_part[interface.part_name] = [] self._by_part[interface.part_name].append(interface.name) # Index by type if interface.interface_type not in self._by_type: self._by_type[interface.interface_type] = [] self._by_type[interface.interface_type].append(interface.name)
[docs] def register_many(self, interfaces: List[InterfaceVolume]) -> None: """Register multiple interface volumes. Args: interfaces: List of InterfaceVolume objects to register """ for interface in interfaces: self.register(interface)
[docs] def unregister(self, name: str) -> Optional[InterfaceVolume]: """Remove an interface from the registry. Args: name: Interface name to remove Returns: The removed interface, or None if not found """ if name not in self._interfaces: return None interface = self._interfaces.pop(name) # Remove from part index if interface.part_name in self._by_part: self._by_part[interface.part_name].remove(name) if not self._by_part[interface.part_name]: del self._by_part[interface.part_name] # Remove from type index if interface.interface_type in self._by_type: self._by_type[interface.interface_type].remove(name) if not self._by_type[interface.interface_type]: del self._by_type[interface.interface_type] return interface
[docs] def get(self, name: str) -> Optional[InterfaceVolume]: """Get interface by name. Args: name: Interface name to look up Returns: InterfaceVolume if found, None otherwise """ return self._interfaces.get(name)
[docs] def get_interfaces_for_part(self, part_name: str) -> List[InterfaceVolume]: """Get all interfaces belonging to a part. Args: part_name: Name of the part Returns: List of InterfaceVolume objects for the part (may be empty) """ names = self._by_part.get(part_name, []) return [self._interfaces[n] for n in names]
[docs] def get_interfaces_by_type(self, interface_type: InterfaceType) -> List[InterfaceVolume]: """Get all interfaces of a specific type. Args: interface_type: Type of interface to find Returns: List of matching InterfaceVolume objects (may be empty) """ names = self._by_type.get(interface_type, []) return [self._interfaces[n] for n in names]
[docs] def has_interfaces(self, part_name: str) -> bool: """Check if a part has any registered interfaces. Args: part_name: Name of the part Returns: True if part has one or more registered interfaces """ return part_name in self._by_part and len(self._by_part[part_name]) > 0
[docs] def find_compatible_interfaces( self, interface: InterfaceVolume, candidates: List[InterfaceVolume] = None ) -> List[Tuple[InterfaceVolume, CompatibilityResult]]: """Find all interfaces compatible with the given interface. Args: interface: Interface to find matches for candidates: List of candidates to check (default: all registered) Returns: List of (interface, compatibility_result) tuples for compatible interfaces """ if candidates is None: candidates = list(self._interfaces.values()) compatible = [] for other in candidates: if other.name == interface.name: continue result = interface.check_compatibility(other) if result.is_compatible: compatible.append((other, result)) return compatible
[docs] def check_overlap_compatibility( self, part_a: str, part_b: str, overlap_region: Optional[Tuple[Any, Any]] = None ) -> Tuple[bool, List[CompatibilityResult]]: """Check if overlap between two parts is due to compatible interfaces. This is the main entry point for the collision detector to check whether detected overlap should be allowed (expected interface) or flagged as a collision error. Args: part_a: First part name part_b: Second part name overlap_region: Optional (center, size) of overlap bounding box for spatial filtering (not yet implemented) Returns: Tuple of: - bool: True if ALL overlapping interfaces are compatible - List[CompatibilityResult]: Results for each interface pair checked Example: >>> is_ok, results = registry.check_overlap_compatibility("SUN", "PLANET") >>> if is_ok: ... print("Expected gear mesh overlap") ... else: ... for r in results: ... if not r.is_compatible: ... print(f"Incompatible: {r.reason}") """ interfaces_a = self.get_interfaces_for_part(part_a) interfaces_b = self.get_interfaces_for_part(part_b) if not interfaces_a or not interfaces_b: # No interfaces defined - overlap is a collision return (False, []) results = [] found_compatible = False found_incompatible = False for iface_a in interfaces_a: for iface_b in interfaces_b: # Check spatial overlap between interfaces if not iface_a.overlaps_with(iface_b): continue # Check compatibility result = iface_a.check_compatibility(iface_b) results.append(result) if result.is_compatible: found_compatible = True else: found_incompatible = True # If any overlapping interfaces are compatible and none are incompatible, # the overlap is allowed if found_compatible and not found_incompatible: return (True, results) # If we found both compatible and incompatible, or only incompatible, # treat as collision return (False, results)
[docs] def get_compatible_interface_names( self, part_a: str, part_b: str ) -> List[str]: """Get names of compatible interfaces between two parts. Convenience method to get just the interface names for parts that have compatible overlap. Args: part_a: First part name part_b: Second part name Returns: List of compatible interface names (from both parts) """ is_compatible, results = self.check_overlap_compatibility(part_a, part_b) if not is_compatible: return [] # Collect interface names from compatible results names = set() interfaces_a = self.get_interfaces_for_part(part_a) interfaces_b = self.get_interfaces_for_part(part_b) for iface_a in interfaces_a: for iface_b in interfaces_b: if iface_a.overlaps_with(iface_b): result = iface_a.check_compatibility(iface_b) if result.is_compatible: names.add(iface_a.name) names.add(iface_b.name) return list(names)
[docs] def get_all_interfaces(self) -> List[InterfaceVolume]: """Get all registered interfaces. Returns: List of all InterfaceVolume objects in the registry """ return list(self._interfaces.values())
[docs] def get_all_parts(self) -> List[str]: """Get names of all parts with registered interfaces. Returns: List of part names """ return list(self._by_part.keys())
[docs] def clear(self) -> None: """Remove all registered interfaces.""" self._interfaces.clear() self._by_part.clear() self._by_type.clear()
[docs] def to_dict(self) -> Dict[str, Any]: """Convert registry to dictionary for serialization. Returns: Dictionary with all interfaces serialized """ return { "interfaces": [iface.to_dict() for iface in self._interfaces.values()] }
def __len__(self) -> int: """Number of registered interfaces.""" return len(self._interfaces) def __contains__(self, name: str) -> bool: """Check if interface name is registered.""" return name in self._interfaces def __iter__(self): """Iterate over all registered interfaces.""" return iter(self._interfaces.values()) def __repr__(self) -> str: """String representation for debugging.""" return ( f"InterfaceRegistry({len(self)} interfaces, " f"{len(self._by_part)} parts, " f"{len(self._by_type)} types)" )