feat: Replace SDF kernel with OpenCASCADE, VTK with pygfx
Major architecture migration: - Remove SDF-based geometry kernel (sdf/) - Remove VTK renderer (drawing_modules/) - Remove old mesh modules (mesh_modules/) New components: - geometry/base.py: Abstract geometry kernel interface - geometry_occ/kernel.py: OpenCASCADE implementation via CadQuery/OCP - geometry_occ/sketch.py: 2D sketching with constraint solving - rendering/base.py: Abstract renderer interface - rendering/pygfx_renderer.py: WebGPU-based renderer - models/data_model.py: Project, Component, Sketch, Body classes - main.py: New Qt-based application Features: - STEP/IGES import/export - Exact BRep geometry (vs approximate SDF mesh) - Parametric sketching with constraints - Boolean operations (union, difference, intersection) - Fillet and chamfer operations - Modern pygfx renderer (~30MB vs VTK ~200MB) Dependencies: - cadquery >= 2.4 - ocp >= 7.9.3 - pygfx >= 0.7.0 - wgpu >= 0.19.0 - PySide6 >= 6.9.0
This commit is contained in:
@@ -0,0 +1,437 @@
|
||||
"""
|
||||
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_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
|
||||
Reference in New Issue
Block a user