Files
fluencyCAD/src/fluency/geometry/base.py
T
2026-07-12 22:21:20 +02:00

443 lines
12 KiB
Python

"""
Geometry abstraction layer for Fluency CAD.
This module defines abstract interfaces for geometry operations,
allowing different geometry kernels to be used interchangeably.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Tuple, Optional, Any, Dict
import numpy as np
@dataclass
class Point2D:
"""2D point representation."""
x: float
y: float
def to_tuple(self) -> Tuple[float, float]:
return (self.x, self.y)
def to_array(self) -> np.ndarray:
return np.array([self.x, self.y])
def distance_to(self, other: "Point2D") -> float:
return np.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Point2D):
return False
return abs(self.x - other.x) < 1e-6 and abs(self.y - other.y) < 1e-6
@dataclass
class Point3D:
"""3D point representation."""
x: float
y: float
z: float
def to_tuple(self) -> Tuple[float, float, float]:
return (self.x, self.y, self.z)
def to_array(self) -> np.ndarray:
return np.array([self.x, self.y, self.z])
def distance_to(self, other: "Point3D") -> float:
return np.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2 + (self.z - other.z) ** 2)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Point3D):
return False
return (
abs(self.x - other.x) < 1e-6
and abs(self.y - other.y) < 1e-6
and abs(self.z - other.z) < 1e-6
)
class GeometryObject:
"""Base class for geometry objects."""
def __init__(self, shape: Any = None, metadata: Optional[Dict] = None):
self.shape = shape
self.metadata = metadata or {}
self._mesh_cache: Optional[Tuple[np.ndarray, np.ndarray]] = None
def invalidate_cache(self) -> None:
"""Invalidate any cached data."""
self._mesh_cache = None
class SketchEntity:
"""Base class for sketch entities (points, lines, circles)."""
def __init__(self, entity_id: int, entity_type: str):
self.id = entity_id
self.entity_type = entity_type
self.constraints: List[str] = []
self.is_construction: bool = False
def add_constraint(self, constraint_type: str) -> None:
self.constraints.append(constraint_type)
class GeometryKernel(ABC):
"""
Abstract base class for geometry kernels.
A geometry kernel provides primitives, operations, and export capabilities
for CAD geometry.
"""
@abstractmethod
def create_point(self, x: float, y: float) -> GeometryObject:
"""Create a 2D point."""
pass
@abstractmethod
def create_line(self, start: Point2D, end: Point2D) -> GeometryObject:
"""Create a 2D line segment."""
pass
@abstractmethod
def create_circle(self, center: Point2D, radius: float) -> GeometryObject:
"""Create a 2D circle."""
pass
@abstractmethod
def create_arc(
self, center: Point2D, radius: float, start_angle: float, end_angle: float
) -> GeometryObject:
"""Create a 2D arc."""
pass
@abstractmethod
def create_polygon(self, points: List[Point2D]) -> GeometryObject:
"""Create a closed polygon from points."""
pass
@abstractmethod
def create_rectangle(
self, width: float, height: float, center: Optional[Point2D] = None
) -> GeometryObject:
"""Create a rectangle."""
pass
@abstractmethod
def extrude(
self,
sketch: GeometryObject,
height: float,
direction: Tuple[float, float, float] = (0, 0, 1),
symmetric: bool = False,
) -> GeometryObject:
"""Extrude a 2D sketch into a 3D solid."""
pass
@abstractmethod
def revolve(
self,
sketch: GeometryObject,
angle: float = 360.0,
axis: Tuple[float, float, float] = (0, 0, 1),
origin: Tuple[float, float, float] = (0, 0, 0),
) -> GeometryObject:
"""Revolve a 2D sketch around an axis."""
pass
@abstractmethod
def loft(self, profiles: List[GeometryObject], ruled: bool = False) -> GeometryObject:
"""Create a loft between multiple profiles."""
pass
@abstractmethod
def sweep(
self, profile: GeometryObject, path: GeometryObject, is_frenet: bool = False
) -> GeometryObject:
"""Sweep a profile along a path."""
pass
@abstractmethod
def boolean_union(self, *bodies: GeometryObject) -> GeometryObject:
"""Union multiple bodies."""
pass
@abstractmethod
def boolean_difference(self, base: GeometryObject, tool: GeometryObject) -> GeometryObject:
"""Subtract tool from base."""
pass
@abstractmethod
def boolean_intersection(self, body1: GeometryObject, body2: GeometryObject) -> GeometryObject:
"""Intersect two bodies."""
pass
@abstractmethod
def fillet(
self, body: GeometryObject, radius: float, edges: Optional[List[Any]] = None
) -> GeometryObject:
"""Apply fillet to edges."""
pass
@abstractmethod
def chamfer(
self, body: GeometryObject, size: float, edges: Optional[List[Any]] = None
) -> GeometryObject:
"""Apply chamfer to edges."""
pass
@abstractmethod
def shell(
self, body: GeometryObject, thickness: float, faces_to_remove: Optional[List[Any]] = None
) -> GeometryObject:
"""Create a shell (hollow body)."""
pass
@abstractmethod
def offset(self, face: GeometryObject, distance: float) -> GeometryObject:
"""Offset a face or surface."""
pass
@abstractmethod
def translate(self, body: GeometryObject, vector: Tuple[float, float, float]) -> GeometryObject:
"""Translate a body."""
pass
@abstractmethod
def rotate(
self,
body: GeometryObject,
axis: Tuple[float, float, float],
angle: float,
origin: Tuple[float, float, float] = (0, 0, 0),
) -> GeometryObject:
"""Rotate a body around an axis."""
pass
@abstractmethod
def scale(self, body: GeometryObject, factor: float) -> GeometryObject:
"""Scale a body uniformly."""
pass
@abstractmethod
def mirror(
self,
body: GeometryObject,
plane_normal: Tuple[float, float, float],
plane_origin: Tuple[float, float, float] = (0, 0, 0),
) -> GeometryObject:
"""Mirror a body across a plane."""
pass
@abstractmethod
def export_step(self, body: GeometryObject, filepath: str, schema: str = "AP214") -> bool:
"""Export to STEP format."""
pass
@abstractmethod
def export_iges(self, body: GeometryObject, filepath: str) -> bool:
"""Export to IGES format."""
pass
@abstractmethod
def export_stl(
self, body: GeometryObject, filepath: str, tolerance: float = 0.1, ascii_mode: bool = False
) -> bool:
"""Export to STL format."""
pass
@abstractmethod
def import_step(self, filepath: str) -> GeometryObject:
"""Import from STEP format."""
pass
@abstractmethod
def import_iges(self, filepath: str) -> GeometryObject:
"""Import from IGES format."""
pass
@abstractmethod
def get_mesh(
self, body: GeometryObject, tolerance: float = 0.1
) -> Tuple[np.ndarray, np.ndarray]:
"""
Get triangulated mesh for rendering.
Returns:
Tuple of (vertices, faces) where:
- vertices: Nx3 numpy array of vertex positions
- faces: Mx3 numpy array of triangle indices
"""
pass
@abstractmethod
def get_edges(self, body: GeometryObject) -> Tuple[np.ndarray, np.ndarray]:
"""
Get edge wireframe for rendering.
Returns:
Tuple of (vertices, edges) where:
- vertices: Nx3 numpy array of vertex positions
- edges: Mx2 numpy array of edge vertex indices
"""
pass
@abstractmethod
def get_bounding_box(self, body: GeometryObject) -> Tuple[Point3D, Point3D]:
"""Get the bounding box of a body."""
pass
@abstractmethod
def get_volume(self, body: GeometryObject) -> float:
"""Calculate the volume of a solid body."""
pass
@abstractmethod
def get_surface_area(self, body: GeometryObject) -> float:
"""Calculate the surface area of a body."""
pass
@abstractmethod
def get_center_of_mass(self, body: GeometryObject) -> Point3D:
"""Calculate the center of mass of a solid body."""
pass
class SketchInterface(ABC):
"""
Abstract interface for 2D sketching with constraints.
A sketch provides 2D geometry creation and constraint solving
capabilities for parametric CAD.
"""
@abstractmethod
def add_point(self, x: float, y: float) -> SketchEntity:
"""Add a point to the sketch."""
pass
@abstractmethod
def add_line(self, start: SketchEntity, end: SketchEntity) -> SketchEntity:
"""Add a line between two points."""
pass
@abstractmethod
def add_circle(self, center: SketchEntity, radius: float) -> SketchEntity:
"""Add a circle."""
pass
@abstractmethod
def add_arc(
self,
center: SketchEntity,
radius: float,
start_point: SketchEntity,
end_point: SketchEntity,
) -> SketchEntity:
"""Add an arc."""
pass
@abstractmethod
def add_rectangle(
self, corner1: Tuple[float, float], corner2: Tuple[float, float]
) -> List[SketchEntity]:
"""Add a rectangle, returning the created entities."""
pass
@abstractmethod
def constrain_coincident(self, *entities: SketchEntity) -> bool:
"""Make entities coincident."""
pass
@abstractmethod
def constrain_horizontal(self, line: SketchEntity) -> bool:
"""Constrain a line to be horizontal."""
pass
@abstractmethod
def constrain_vertical(self, line: SketchEntity) -> bool:
"""Constrain a line to be vertical."""
pass
@abstractmethod
def constrain_distance(
self, entity1: SketchEntity, entity2: SketchEntity, distance: float
) -> bool:
"""Constrain distance between two entities."""
pass
@abstractmethod
def constrain_angle(self, line1: SketchEntity, line2: SketchEntity, angle: float) -> bool:
"""Constrain angle between two lines."""
pass
@abstractmethod
def constrain_parallel(self, line1: SketchEntity, line2: SketchEntity) -> bool:
"""Constrain two lines to be parallel."""
pass
@abstractmethod
def constrain_perpendicular(self, line1: SketchEntity, line2: SketchEntity) -> bool:
"""Constrain two lines to be perpendicular."""
pass
@abstractmethod
def constrain_midpoint(self, point: SketchEntity, line: SketchEntity) -> bool:
"""Constrain a point to be at the midpoint of a line."""
pass
@abstractmethod
def constrain_tangent(self, entity1: SketchEntity, entity2: SketchEntity) -> bool:
"""Constrain two entities to be tangent."""
pass
@abstractmethod
def constrain_equal_length(self, line1: SketchEntity, line2: SketchEntity) -> bool:
"""Constrain two lines to have equal length."""
pass
@abstractmethod
def constrain_equal_radius(self, circle1: SketchEntity, circle2: SketchEntity) -> bool:
"""Constrain two circles to have equal radius."""
pass
@abstractmethod
def constrain_diameter(self, circle: SketchEntity, diameter: float) -> bool:
"""Set the diameter of a circle."""
pass
@abstractmethod
def constrain_fixed(self, entity: SketchEntity) -> bool:
"""Fix an entity in place."""
pass
@abstractmethod
def solve(self) -> bool:
"""Solve all constraints."""
pass
@abstractmethod
def get_geometry(self) -> GeometryObject:
"""Get the solved geometry for operations."""
pass
@abstractmethod
def get_points(self) -> List[Point2D]:
"""Get all point positions."""
pass
@abstractmethod
def clear(self) -> None:
"""Clear all geometry and constraints."""
pass
@abstractmethod
def delete_entity(self, entity: SketchEntity) -> bool:
"""Delete an entity and its constraints."""
pass