Files
fluencyCAD/src/fluency/rendering/base.py
T
bklronin d52106a48a fix: update pygfx integration for current API
- Use rendercanvas.qt.RenderCanvas instead of wgpu.gui.qt.WgpuCanvas
- Fix camera position API: use camera.local.position instead of camera.position.set()
- Fix light position API: use light.local.position
- Fix PygfxRenderObject to properly inherit from RenderObject
- Change add_mesh/add_wireframe/add_points/add_grid/add_axes to return string ID
- Update base Renderer class to return str instead of RenderObject
- Add custom compute_normals() function for mesh normals
2026-03-14 09:06:38 +01:00

381 lines
9.6 KiB
Python

"""
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