""" Data models for Fluency CAD. This module defines the core data structures for the CAD application including projects, components, sketches, and bodies. """ from dataclasses import dataclass, field from typing import Dict, List, Optional, Any from datetime import datetime import uuid import numpy as np from fluency.geometry.base import ( Point2D, Point3D, GeometryObject, SketchInterface, ) from fluency.geometry_occ.kernel import OCGeometryKernel, OCCGeometryObject from fluency.geometry_occ.sketch import OCCSketch @dataclass class Sketch: """ 2D sketch with constraints. A sketch contains 2D geometry on a workplane that can be extruded or revolved to create 3D bodies. """ id: str = field(default_factory=lambda: str(uuid.uuid4())) name: str = "Untitled Sketch" workplane_origin: np.ndarray = field(default_factory=lambda: np.array([0.0, 0.0, 0.0])) workplane_normal: np.ndarray = field(default_factory=lambda: np.array([0.0, 0.0, 1.0])) workplane_x_dir: np.ndarray = field(default_factory=lambda: np.array([1.0, 0.0, 0.0])) occ_sketch: Optional[OCCSketch] = field(default_factory=OCCSketch) geometry: Optional[OCCGeometryObject] = None is_solved: bool = False is_fully_constrained: bool = False created_at: datetime = field(default_factory=datetime.now) modified_at: datetime = field(default_factory=datetime.now) def add_point(self, x: float, y: float) -> Any: """Add a point to the sketch.""" self.modified_at = datetime.now() self.is_solved = False if self.occ_sketch: return self.occ_sketch.add_point(x, y) return None def add_line(self, start: Any, end: Any) -> Any: """Add a line to the sketch.""" self.modified_at = datetime.now() self.is_solved = False if self.occ_sketch: return self.occ_sketch.add_line(start, end) return None def add_circle(self, center: Any, radius: float) -> Any: """Add a circle to the sketch.""" self.modified_at = datetime.now() self.is_solved = False if self.occ_sketch: return self.occ_sketch.add_circle(center, radius) return None def add_rectangle(self, corner1: tuple, corner2: tuple) -> List[Any]: """Add a rectangle to the sketch.""" self.modified_at = datetime.now() self.is_solved = False if self.occ_sketch: return self.occ_sketch.add_rectangle(corner1, corner2) return [] def solve(self) -> bool: """Solve all constraints.""" if self.occ_sketch: result = self.occ_sketch.solve() self.is_solved = result self.is_fully_constrained = self.occ_sketch.is_fully_constrained() self.modified_at = datetime.now() return result return False def get_geometry(self) -> Optional[GeometryObject]: """Get the solved geometry.""" if self.occ_sketch: return self.occ_sketch.get_geometry() return None def get_polygon_points(self) -> List[Point2D]: """Get ordered polygon points.""" if self.occ_sketch: return self.occ_sketch.get_polygon_points() return [] def clear(self) -> None: """Clear all geometry.""" if self.occ_sketch: self.occ_sketch.clear() self.geometry = None self.is_solved = False self.is_fully_constrained = False self.modified_at = datetime.now() @dataclass class Body: """ 3D solid body. A body is created from a sketch through operations like extrude, revolve, loft, or sweep. """ id: str = field(default_factory=lambda: str(uuid.uuid4())) name: str = "Untitled Body" geometry: Optional[OCCGeometryObject] = None source_sketch: Optional[Sketch] = None source_operation: str = "extrude" position: np.ndarray = field(default_factory=lambda: np.array([0.0, 0.0, 0.0])) rotation: np.ndarray = field(default_factory=lambda: np.eye(3)) color: tuple = (0.2, 0.4, 0.8) opacity: float = 1.0 visible: bool = True render_object: Any = None created_at: datetime = field(default_factory=datetime.now) modified_at: datetime = field(default_factory=datetime.now) def get_mesh(self, kernel: OCGeometryKernel, tolerance: float = 0.1) -> tuple: """Get mesh for rendering.""" if self.geometry and kernel: return kernel.get_mesh(self.geometry, tolerance) return np.array([]), np.array([]) def get_edges(self, kernel: OCGeometryKernel) -> tuple: """Get edges for wireframe rendering.""" if self.geometry and kernel: return kernel.get_edges(self.geometry) return np.array([]), np.array([]) @dataclass class Component: """ Component containing sketches and bodies. A component is a logical grouping of geometry, similar to a part in a CAD system. """ id: str = field(default_factory=lambda: str(uuid.uuid4())) name: str = "Untitled Component" description: str = "" sketches: Dict[str, Sketch] = field(default_factory=dict) bodies: Dict[str, Body] = field(default_factory=dict) active_sketch: Optional[str] = None created_at: datetime = field(default_factory=datetime.now) modified_at: datetime = field(default_factory=datetime.now) def add_sketch(self, sketch: Optional[Sketch] = None) -> Sketch: """Add a sketch to the component.""" if sketch is None: sketch = Sketch(name=f"Sketch {len(self.sketches) + 1}") self.sketches[sketch.id] = sketch self.modified_at = datetime.now() return sketch def add_body(self, body: Optional[Body] = None) -> Body: """Add a body to the component.""" if body is None: body = Body(name=f"Body {len(self.bodies) + 1}") self.bodies[body.id] = body self.modified_at = datetime.now() return body def remove_sketch(self, sketch_id: str) -> bool: """Remove a sketch from the component.""" if sketch_id in self.sketches: del self.sketches[sketch_id] if self.active_sketch == sketch_id: self.active_sketch = None self.modified_at = datetime.now() return True return False def remove_body(self, body_id: str) -> bool: """Remove a body from the component.""" if body_id in self.bodies: del self.bodies[body_id] self.modified_at = datetime.now() return True return False def get_active_sketch(self) -> Optional[Sketch]: """Get the currently active sketch.""" if self.active_sketch and self.active_sketch in self.sketches: return self.sketches[self.active_sketch] return None def set_active_sketch(self, sketch_id: Optional[str]) -> None: """Set the active sketch.""" self.active_sketch = sketch_id self.modified_at = datetime.now() @dataclass class Project: """ Top-level project container. A project contains components and provides access to the geometry kernel for operations. """ name: str = "Untitled Project" description: str = "" components: Dict[str, Component] = field(default_factory=dict) active_component: Optional[str] = None kernel: OCGeometryKernel = field(default_factory=OCGeometryKernel) created_at: datetime = field(default_factory=datetime.now) modified_at: datetime = field(default_factory=datetime.now) file_path: Optional[str] = None def add_component(self, component: Optional[Component] = None) -> Component: """Add a component to the project.""" if component is None: component = Component(name=f"Component {len(self.components) + 1}") self.components[component.id] = component if self.active_component is None: self.active_component = component.id self.modified_at = datetime.now() return component def remove_component(self, component_id: str) -> bool: """Remove a component from the project.""" if component_id in self.components: del self.components[component_id] if self.active_component == component_id: self.active_component = next(iter(self.components.keys()), None) self.modified_at = datetime.now() return True return False def get_active_component(self) -> Optional[Component]: """Get the currently active component.""" if self.active_component and self.active_component in self.components: return self.components[self.active_component] return None def set_active_component(self, component_id: Optional[str]) -> None: """Set the active component.""" self.active_component = component_id self.modified_at = datetime.now() def export_step(self, filepath: str) -> bool: """Export all visible bodies to STEP.""" all_bodies: List[OCCGeometryObject] = [] for comp in self.components.values(): for body in comp.bodies.values(): if body.visible and body.geometry: all_bodies.append(body.geometry) if not all_bodies: return False if len(all_bodies) == 1: return self.kernel.export_step(all_bodies[0], filepath) result = self.kernel.boolean_union(*all_bodies) return self.kernel.export_step(result, filepath) def export_iges(self, filepath: str) -> bool: """Export all visible bodies to IGES.""" all_bodies: List[OCCGeometryObject] = [] for comp in self.components.values(): for body in comp.bodies.values(): if body.visible and body.geometry: all_bodies.append(body.geometry) if not all_bodies: return False if len(all_bodies) == 1: return self.kernel.export_iges(all_bodies[0], filepath) result = self.kernel.boolean_union(*all_bodies) return self.kernel.export_iges(result, filepath) def export_stl(self, filepath: str, tolerance: float = 0.1) -> bool: """Export all visible bodies to STL.""" all_bodies: List[OCCGeometryObject] = [] for comp in self.components.values(): for body in comp.bodies.values(): if body.visible and body.geometry: all_bodies.append(body.geometry) if not all_bodies: return False if len(all_bodies) == 1: return self.kernel.export_stl(all_bodies[0], filepath, tolerance) result = self.kernel.boolean_union(*all_bodies) return self.kernel.export_stl(result, filepath, tolerance) def get_all_bodies(self) -> List[Body]: """Get all bodies from all components.""" bodies: List[Body] = [] for comp in self.components.values(): bodies.extend(comp.bodies.values()) return bodies def get_all_sketches(self) -> List[Sketch]: """Get all sketches from all components.""" sketches: List[Sketch] = [] for comp in self.components.values(): sketches.extend(comp.sketches.values()) return sketches