d52106a48a
- 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
381 lines
9.6 KiB
Python
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
|