""" Rendering abstraction layer for Fluency CAD. This module defines abstract interfaces for 3D rendering, allowing different rendering backends to be used interchangeably. """ from abc import ABC, abstractmethod from typing import List, Tuple, Optional, Callable, Any from dataclasses import dataclass import numpy as np @dataclass class RenderColor: """RGB color representation.""" r: float g: float b: float a: float = 1.0 def to_tuple(self) -> Tuple[float, float, float, float]: return (self.r, self.g, self.b, self.a) def to_tuple_rgb(self) -> Tuple[float, float, float]: return (self.r, self.g, self.b) @classmethod def from_hex(cls, hex_color: str) -> "RenderColor": """Create color from hex string (#RRGGBB or #RRGGBBAA).""" hex_color = hex_color.lstrip("#") if len(hex_color) == 6: r = int(hex_color[0:2], 16) / 255.0 g = int(hex_color[2:4], 16) / 255.0 b = int(hex_color[4:6], 16) / 255.0 return cls(r, g, b) elif len(hex_color) == 8: r = int(hex_color[0:2], 16) / 255.0 g = int(hex_color[2:4], 16) / 255.0 b = int(hex_color[4:6], 16) / 255.0 a = int(hex_color[6:8], 16) / 255.0 return cls(r, g, b, a) raise ValueError(f"Invalid hex color: {hex_color}") class RenderObject: """Base class for renderable objects.""" def __init__(self, name: Optional[str] = None): self.name = name self.visible: bool = True self.selected: bool = False self.color: RenderColor = RenderColor(0.2, 0.4, 0.8) self._scene_node: Any = None def set_color(self, color: RenderColor) -> None: self.color = color def set_visible(self, visible: bool) -> None: self.visible = visible def set_selected(self, selected: bool) -> None: self.selected = selected class Renderer(ABC): """ Abstract base class for 3D renderers. A renderer provides 3D visualization capabilities including mesh display, camera control, and object selection. """ @abstractmethod def initialize(self, parent_widget: Any) -> bool: """ Initialize the renderer with a parent widget. Args: parent_widget: Qt widget to embed the renderer in Returns: True if initialization succeeded """ pass @abstractmethod def shutdown(self) -> None: """Clean up renderer resources.""" pass @abstractmethod def add_mesh( self, vertices: np.ndarray, faces: np.ndarray, color: Tuple[float, float, float] = (0.2, 0.4, 0.8), name: Optional[str] = None, ) -> str: """ Add a mesh to the scene. Args: vertices: Nx3 array of vertex positions faces: Mx3 array of triangle indices color: RGB color tuple name: Optional name for the object Returns: String ID of the mesh """ pass @abstractmethod def add_wireframe( self, vertices: np.ndarray, edges: np.ndarray, color: Tuple[float, float, float] = (1.0, 1.0, 1.0), line_width: float = 1.0, name: Optional[str] = None, ) -> str: """ Add a wireframe to the scene. Args: vertices: Nx3 array of vertex positions edges: Mx2 array of edge vertex indices color: RGB color tuple line_width: Width of lines name: Optional name for the object Returns: String ID of the wireframe """ pass @abstractmethod def add_points( self, points: np.ndarray, color: Tuple[float, float, float] = (1.0, 0.0, 0.0), size: float = 5.0, name: Optional[str] = None, ) -> str: """ Add points to the scene. Args: points: Nx3 array of point positions color: RGB color tuple size: Point size name: Optional name for the object Returns: String ID of the points """ pass @abstractmethod def add_lines( self, start_points: np.ndarray, end_points: np.ndarray, color: Tuple[float, float, float] = (1.0, 1.0, 1.0), line_width: float = 1.0, name: Optional[str] = None, ) -> str: """ Add line segments to the scene. Args: start_points: Nx3 array of line start positions end_points: Nx3 array of line end positions color: RGB color tuple line_width: Width of lines name: Optional name for the object Returns: String ID of the lines """ pass @abstractmethod def remove_object(self, obj: RenderObject) -> bool: """ Remove an object from the scene. Args: obj: Object to remove Returns: True if removal succeeded """ pass @abstractmethod def clear_scene(self) -> None: """Remove all objects from the scene.""" pass @abstractmethod def update_mesh(self, obj: RenderObject, vertices: np.ndarray, faces: np.ndarray) -> bool: """ Update mesh geometry. Args: obj: Object to update vertices: New Nx3 array of vertex positions faces: New Mx3 array of triangle indices Returns: True if update succeeded """ pass @abstractmethod def set_object_color(self, obj: RenderObject, color: Tuple[float, float, float]) -> None: """Set the color of an object.""" pass @abstractmethod def set_object_visible(self, obj: RenderObject, visible: bool) -> None: """Set the visibility of an object.""" pass @abstractmethod def set_camera_position( self, position: Tuple[float, float, float], target: Tuple[float, float, float] = (0, 0, 0), up: Tuple[float, float, float] = (0, 0, 1), ) -> None: """ Set camera position and orientation. Args: position: Camera position target: Point camera is looking at up: Up vector """ pass @abstractmethod def get_camera_position(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Get camera position, target, and up vector. Returns: Tuple of (position, target, up) as numpy arrays """ pass @abstractmethod def fit_camera(self, padding: float = 1.1) -> None: """ Fit camera to show all objects. Args: padding: Padding factor (1.0 = exact fit) """ pass @abstractmethod def set_camera_perspective( self, fov: float = 50.0, near: float = 0.1, far: float = 10000.0 ) -> None: """Set camera perspective parameters.""" pass @abstractmethod def set_camera_orthographic( self, width: float = 100.0, near: float = 0.1, far: float = 10000.0 ) -> None: """Set camera orthographic parameters.""" pass @abstractmethod def render(self) -> None: """Trigger a render.""" pass @abstractmethod def on_pick(self, callback: Callable[[Any], None]) -> None: """ Register a callback for picking/selection. Args: callback: Function called with pick info when object is clicked """ pass @abstractmethod def on_camera_change(self, callback: Callable[[], None]) -> None: """ Register a callback for camera changes. Args: callback: Function called when camera moves """ pass @abstractmethod def set_background_color(self, color: Tuple[float, float, float]) -> None: """Set the background color.""" pass @abstractmethod def add_grid( self, size: float = 100.0, divisions: int = 10, color: Tuple[float, float, float] = (0.3, 0.3, 0.3), ) -> str: """Add a reference grid. Returns the grid ID.""" pass @abstractmethod def add_axes(self, size: float = 10.0, visible: bool = True) -> str: """Add coordinate axes. Returns the axes ID.""" pass @abstractmethod def get_screen_size(self) -> Tuple[int, int]: """Get the screen size in pixels.""" pass @abstractmethod def project_to_screen(self, point: Tuple[float, float, float]) -> Tuple[int, int]: """ Project a 3D point to screen coordinates. Args: point: 3D point to project Returns: Screen (x, y) coordinates """ pass @abstractmethod def unproject_from_screen( self, screen_x: int, screen_y: int, depth: float = 0.0 ) -> Tuple[float, float, float]: """ Unproject screen coordinates to 3D. Args: screen_x: Screen x coordinate screen_y: Screen y coordinate depth: Depth value (0=near, 1=far) Returns: 3D point coordinates """ pass @abstractmethod def take_screenshot(self) -> np.ndarray: """ Take a screenshot of the current view. Returns: RGBA image as numpy array """ pass @abstractmethod def save_screenshot(self, filepath: str) -> bool: """ Save a screenshot to file. Args: filepath: Path to save screenshot Returns: True if save succeeded """ pass