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,380 @@
|
||||
"""
|
||||
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,
|
||||
) -> RenderObject:
|
||||
"""
|
||||
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:
|
||||
RenderObject representing 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,
|
||||
) -> RenderObject:
|
||||
"""
|
||||
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:
|
||||
RenderObject representing 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,
|
||||
) -> RenderObject:
|
||||
"""
|
||||
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:
|
||||
RenderObject representing 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,
|
||||
) -> RenderObject:
|
||||
"""
|
||||
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:
|
||||
RenderObject representing 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),
|
||||
) -> RenderObject:
|
||||
"""Add a reference grid."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def add_axes(self, size: float = 10.0, visible: bool = True) -> RenderObject:
|
||||
"""Add coordinate axes."""
|
||||
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
|
||||
Reference in New Issue
Block a user