Welcome to Hackster!
Hackster is a community dedicated to learning hardware, from beginner to pro. Join us, it's free!
Auxence DaillenMohit Chopra
Published

3D Hand Canvas

The project enables you to draw, sculpt, and do pottery in 3D using your hand gestures, view it in a hologram and export for 3D prints.

IntermediateWork in progress70

Things used in this project

Hardware components

DIY Hologram Box
×1
Webcam
×1

Software apps and online services

python
mediapipe
OpenCV
OpenCV
pygames
blender
p5js

Story

Read more

Code

Final Code.py

Python
import cv2
import mediapipe as mp
import pygame
import sys
import traceback
import numpy as np
import math
import os
import subprocess
from datetime import datetime
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
from collections import deque

# Window dimensions
WIDTH, HEIGHT = 1280, 720

# Constants for drawing modes
MODE_LINE = 0
MODE_POTTERY = 1  # Direct 3D sculpting mode
MODE_PROFILE_POTTERY = 2  # Profile-based pottery wheel mode

# MediaPipe configuration
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils

class SmoothingFilter:
    """
    Implements a moving average filter to stabilize hand movements
    """
    def __init__(self, window_size=10):
        self.window_size = window_size
        self.points_buffer = deque(maxlen=window_size)
        self.weights = np.linspace(0.5, 1.0, window_size)  # More weight on recent points
        self.weights = self.weights / np.sum(self.weights)  # Normalize weights
    
    def update(self, new_point):
        """
        Add a new point to the buffer and return the smoothed point
        """
        if new_point is None:
            return None
            
        try:
            # Ensure point is converted to a consistent format
            if isinstance(new_point, (list, tuple, np.ndarray)):
                point = np.array(new_point)
            elif hasattr(new_point, 'x') and hasattr(new_point, 'y') and hasattr(new_point, 'z'):
                point = np.array([new_point.x, new_point.y, new_point.z])
            else:
                print(f"Invalid point type for smoothing: {type(new_point)}")
                return new_point
            
            self.points_buffer.append(point)
            
            # If not enough points yet, return the current point
            if len(self.points_buffer) < 2:
                return point.tolist()
                
            # Calculate weighted average
            point_array = np.array(list(self.points_buffer))
            weights = self.weights[-len(self.points_buffer):]
            weights = weights / np.sum(weights)  # Re-normalize weights
            
            smoothed_point = np.zeros_like(point_array[0], dtype=float)
            for i, pt in enumerate(point_array):
                smoothed_point += pt * weights[i]
                
            return smoothed_point.tolist()
        except Exception as e:
            print(f"Error in smoothing: {e}")
            return new_point
    
    def reset(self):
        """Clear the buffer"""
        self.points_buffer.clear()

class AirCanvasCamera:
    """Camera control system for 3D space navigation with smoothing"""
    
    def __init__(self):
        # Camera position in 3D space
        self.position = np.array([0.0, 0.0, -10.0])
        
        # Camera rotation angles (degrees)
        self.rotation_x = 0.0
        self.rotation_y = 0.0
        
        # Navigation state tracking
        self.is_navigating = False
        self.last_nav_point = None
        
        # Navigation sensitivity
        self.rotation_sensitivity = 20.0  # Reduced for smoother movement
        self.movement_sensitivity = 5.0   # Reduced for smoother movement
        
        # Smoothing filter for camera movement
        self.smoother = SmoothingFilter(window_size=15)
    
    def move(self, dx, dy, dz):
        """Move the camera position with smoothing"""
        try:
            # Apply small damping factor to prevent too sudden movements
            damping = 0.8
            self.position[0] += dx * damping
            self.position[1] += dy * damping
            self.position[2] += dz * damping
        except Exception as e:
            print(f"Camera movement error: {e}")
    
    def rotate(self, dx, dy):
        """Rotate the camera view with smoothing"""
        try:
            # Apply damping for smoother rotation
            damping = 0.5
            self.rotation_y += dx * self.rotation_sensitivity * damping
            self.rotation_x += dy * self.rotation_sensitivity * damping
            
            # Clamp rotation on x-axis to prevent flipping
            self.rotation_x = max(-90, min(90, self.rotation_x))
        except Exception as e:
            print(f"Camera rotation error: {e}")
    
    def start_navigation(self, hand_point):
        """Begin camera navigation using hand tracking"""
        try:
            self.is_navigating = True
            self.last_nav_point = hand_point
            self.smoother.reset()  # Reset smoother for new navigation
        except Exception as e:
            print(f"Navigation start error: {e}")
    
    def navigate(self, hand_point):
        """Update camera based on hand movement with smoothing"""
        try:
            if self.is_navigating and self.last_nav_point:
                # Apply smoothing to the hand point
                smoothed_point = self.smoother.update(hand_point)
                
                if smoothed_point and hasattr(smoothed_point, 'x'):
                    # Calculate movement delta
                    dx = smoothed_point.x - self.last_nav_point.x
                    dy = smoothed_point.y - self.last_nav_point.y
                    
                    # Apply rotation based on hand movement
                    self.rotate(dx * 100, dy * 100)
                    
                    # Update the last navigation point gradually to reduce jitter
                    self.last_nav_point = smoothed_point
                elif isinstance(smoothed_point, (list, np.ndarray)) and len(smoothed_point) >= 2:
                    # For list or numpy array inputs
                    dx = smoothed_point[0] - self.last_nav_point.x
                    dy = smoothed_point[1] - self.last_nav_point.y
                    
                    # Apply rotation based on hand movement
                    self.rotate(dx * 100, dy * 100)
                    
                    # Update the last navigation point
                    self.last_nav_point = type('Point', (), {'x': smoothed_point[0], 'y': smoothed_point[1]})()
        except Exception as e:
            print(f"Navigation update error: {e}")
    
    def stop_navigation(self):
        """End camera navigation"""
        self.is_navigating = False
        self.last_nav_point = None

# Function for robust hand point transformation
def transform_hand_point(landmark, camera):
    """
    Transform hand point considering camera orientation with robust error handling
    
    Args:
        landmark: Mediapipe hand landmark
        camera: AirCanvasCamera instance
    
    Returns:
        List of 3D coordinates with robust error handling
    """
    try:
        # Enhanced input validation
        if not hasattr(landmark, 'x') or not hasattr(landmark, 'y') or not hasattr(landmark, 'z'):
            print("Invalid landmark object")
            return [0, 0, 0]  # Default safe value
            
        # More comprehensive NaN and inf checks
        coordinates = [landmark.x, landmark.y, landmark.z]
        if any(math.isnan(coord) or math.isinf(coord) for coord in coordinates):
            print("Invalid landmark coordinates")
            return [0, 0, 0]  # Default safe value
            
        # Convert hand position to 3D world coordinates
        transformed_point = np.array([
            (landmark.x - 0.5) * 10,  # X
            -(landmark.y - 0.5) * 10, # Y (inverted)
            landmark.z * 10           # Z depth
        ])

        # Rotation matrices with additional error checks
        def safe_rotation_matrix(angle, axis):
            try:
                rads = np.radians(-angle)
                if axis == 'x':
                    return np.array([
                        [1, 0, 0],
                        [0, np.cos(rads), -np.sin(rads)],
                        [0, np.sin(rads), np.cos(rads)]
                    ])
                elif axis == 'y':
                    return np.array([
                        [np.cos(rads), 0, np.sin(rads)],
                        [0, 1, 0],
                        [-np.sin(rads), 0, np.cos(rads)]
                    ])
            except Exception as e:
                print(f"Error creating rotation matrix: {e}")
                return np.eye(3)  # Identity matrix as fallback

        Rx = safe_rotation_matrix(camera.rotation_x, 'x')
        Ry = safe_rotation_matrix(camera.rotation_y, 'y')

        # Apply rotations safely
        try:
            rotated_point = np.dot(Ry, np.dot(Rx, transformed_point))
        except Exception as e:
            print(f"Matrix multiplication error: {e}")
            rotated_point = transformed_point  # Fallback to original point
        
        # Final safety check
        if np.any(np.isnan(rotated_point)) or np.any(np.isinf(rotated_point)):
            print("Invalid result after transformation")
            return [0, 0, 0]  # Default safe value
            
        return list(rotated_point)
    except Exception as e:
        print(f"Comprehensive error in transform_hand_point: {e}")
        return [0, 0, 0]  # Default safe value


class Vertex:
    """
    Represents a vertex in 3D space with capabilities for connecting to other vertices
    """
    def __init__(self, position, snap_radius=0.5):
        self.position = position  # [x, y, z]
        self.connected_to = []    # List of vertices this connects to
        self.snap_radius = snap_radius  # Radius for snapping to other vertices
        
    def try_snap(self, vertices):
        """
        Try to snap this vertex to any nearby vertices
        Returns the vertex it snapped to, or None
        """
        for vertex in vertices:
            if vertex is self:
                continue
                
            # Calculate distance
            dist = np.linalg.norm(np.array(self.position) - np.array(vertex.position))
            
            if dist < self.snap_radius:
                # Snap to this vertex
                self.position = vertex.position.copy()
                return vertex
                
        return None
        
    def add_connection(self, vertex):
        """Add a connection to another vertex"""
        if vertex not in self.connected_to:
            self.connected_to.append(vertex)
            
    def remove_connection(self, vertex):
        """Remove a connection to another vertex"""
        if vertex in self.connected_to:
            self.connected_to.remove(vertex)


class Line3D:
    """
    Enhanced line representation for better 3D structure awareness
    """
    def __init__(self, vertices=None):
        self.vertices = vertices or []  # List of Vertex objects
        self.start_vertex = None
        self.end_vertex = None
        self.is_complete = False
        
    def add_vertex(self, position):
        """Add a vertex to the line"""
        new_vertex = Vertex(position)
        self.vertices.append(new_vertex)
        
        # Track start and end vertices
        if len(self.vertices) == 1:
            self.start_vertex = new_vertex
        else:
            self.end_vertex = new_vertex
            
        return new_vertex
        
    def complete(self):
        """Mark the line as complete"""
        self.is_complete = True


class GridPlane:
    """
    Represents a reference grid plane for easier 3D alignment
    """
    def __init__(self, normal_axis='y', size=10, spacing=1.0):
        self.normal_axis = normal_axis  # Axis perpendicular to the plane ('x', 'y', or 'z')
        self.size = size                # Grid size (extends in both directions)
        self.spacing = spacing          # Grid line spacing
        self.position = 0               # Position along the normal axis
        self.visible = True
        self.snap_enabled = True
        self.snap_tolerance = 0.3       # Tolerance for snapping to the grid plane
        
    def draw(self):
        """Draw the grid plane"""
        if not self.visible:
            return
            
        # Draw grid plane with slightly transparent lines
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        
        glLineWidth(1.0)
        glBegin(GL_LINES)
        
        # Set grid color based on axis (with transparency)
        if self.normal_axis == 'x':
            glColor4f(0.7, 0.3, 0.3, 0.3)  # Reddish for X plane
        elif self.normal_axis == 'y':
            glColor4f(0.3, 0.7, 0.3, 0.3)  # Greenish for Y plane
        else:  # z
            glColor4f(0.3, 0.3, 0.7, 0.3)  # Bluish for Z plane
            
        # Draw the grid lines
        min_coord = -self.size * self.spacing
        max_coord = self.size * self.spacing
        
        if self.normal_axis == 'x':
            # Draw Z lines
            for i in range(-self.size, self.size + 1):
                z = i * self.spacing
                glVertex3f(self.position, min_coord, z)
                glVertex3f(self.position, max_coord, z)
                
            # Draw Y lines
            for i in range(-self.size, self.size + 1):
                y = i * self.spacing
                glVertex3f(self.position, y, min_coord)
                glVertex3f(self.position, y, max_coord)
                
        elif self.normal_axis == 'y':
            # Draw X lines
            for i in range(-self.size, self.size + 1):
                x = i * self.spacing
                glVertex3f(x, self.position, min_coord)
                glVertex3f(x, self.position, max_coord)
                
            # Draw Z lines
            for i in range(-self.size, self.size + 1):
                z = i * self.spacing
                glVertex3f(min_coord, self.position, z)
                glVertex3f(max_coord, self.position, z)
                
        else:  # z
            # Draw X lines
            for i in range(-self.size, self.size + 1):
                x = i * self.spacing
                glVertex3f(x, min_coord, self.position)
                glVertex3f(x, max_coord, self.position)
                
            # Draw Y lines
            for i in range(-self.size, self.size + 1):
                y = i * self.spacing
                glVertex3f(min_coord, y, self.position)
                glVertex3f(max_coord, y, self.position)
        
        glEnd()
        glDisable(GL_BLEND)
        
    def snap_to_grid(self, point):
        """
        Snap a point to the grid if it's close to the plane
        Returns the snapped point
        """
        if not self.snap_enabled:
            return point
            
        # Extract the normal axis coordinate
        if self.normal_axis == 'x':
            axis_idx = 0
        elif self.normal_axis == 'y':
            axis_idx = 1
        else:  # z
            axis_idx = 2
            
        # Check if the point is close to the plane
        if abs(point[axis_idx] - self.position) < self.snap_tolerance:
            # Snap the point to the plane
            snapped_point = point.copy()
            snapped_point[axis_idx] = self.position
            
            # Snap the other coordinates to the grid
            for i in range(3):
                if i != axis_idx:
                    snapped_point[i] = round(snapped_point[i] / self.spacing) * self.spacing
                    
            return snapped_point
            
        return point


class SurfaceGenerator:
    """Advanced surface generation system for creating 3D printable meshes"""

    def __init__(self):
        self.lines = []  # Raw drawing lines (Line3D objects)
        self.vertices = []  # All vertices
        self.surfaces = []  # Generated surfaces
        self.surface_threshold = 0.2  # Distance to consider lines connected
        
        # Direct sculpting pottery mode variables
        self.pottery_mesh = None
        self.pottery_radius = 2.0
        self.pottery_height = 4.0
        self.pottery_slices = 16  # Number of vertical slices
        self.pottery_rings = 12   # Number of horizontal rings
        self.sculpt_mode = 0      # 0 = add material, 1 = subtract material
        self.sculpt_strength = {
            0: 0.3,  # Add strength
            1: -0.3  # Subtract strength (negative)
        }
        
        # Profile-based pottery variables
        self.profile_points = []  # 2D profile points for revolution
        self.pottery_surface_vertices = []  # Vertices of the pottery surface
        self.pottery_surface_faces = []     # Faces of the pottery surface
        self.pottery_surface_normals = []   # Normals for lighting
        self.surface_segments = 36  # Number of segments for revolution
        self.pottery_render_mode = 'solid'  # 'solid', 'wireframe', 'points'
        self.show_profile = True    # Show the profile line
        self.previous_profiles = []  # For undo functionality
        self.max_profiles = 5
        
    def add_line(self, line):
        """Add a new line to the drawing"""
        self.lines.append(line)
        
        # Add vertices to the global list
        for vertex in line.vertices:
            if vertex not in self.vertices:
                self.vertices.append(vertex)
                
    def find_closest_vertex(self, position, max_distance=0.5, exclude=None):
        """
        Find the closest vertex to a given position within a maximum distance
        Returns the vertex or None
        """
        closest_vertex = None
        min_dist = max_distance
        
        for vertex in self.vertices:
            if exclude and vertex is exclude:
                continue
                
            dist = np.linalg.norm(np.array(vertex.position) - np.array(position))
            
            if dist < min_dist:
                min_dist = dist
                closest_vertex = vertex
                
        return closest_vertex
        
    def connect_lines(self, line1, line2):
        """
        Explicitly connect two lines by their endpoints
        Returns True if successful
        """
        if not line1.is_complete or not line2.is_complete:
            return False
            
        # Connect the endpoints
        line1.end_vertex.add_connection(line2.start_vertex)
        line2.start_vertex.add_connection(line1.end_vertex)
        
        return True
    
    def find_closest_line(self, position, max_distance=1.0):
        """
        Find the closest line to a given position
        Returns (line, distance) tuple or (None, None)
        """
        closest_line = None
        min_distance = max_distance
        
        for line in self.lines:
            if len(line.vertices) < 2:
                continue
                
            # Check distance to each segment in the line
            for i in range(len(line.vertices) - 1):
                p1 = np.array(line.vertices[i].position)
                p2 = np.array(line.vertices[i + 1].position)
                p = np.array(position)
                
                # Calculate distance from point to line segment
                d = self._point_to_segment_distance(p, p1, p2)
                
                if d < min_distance:
                    min_distance = d
                    closest_line = line
                    
        return closest_line, min_distance
    
    def _point_to_segment_distance(self, p, p1, p2):
        """Calculate the distance from a point to a line segment"""
        segment = p2 - p1
        segment_length_squared = np.dot(segment, segment)
        
        # Avoid division by zero
        if segment_length_squared == 0:
            return np.linalg.norm(p - p1)
            
        # Calculate projection
        t = max(0, min(1, np.dot(p - p1, segment) / segment_length_squared))
        projection = p1 + t * segment
        
        # Return distance to the projection
        return np.linalg.norm(p - projection)
        
    def find_closed_loops(self):
        """
        Find closed loops of connected vertices that can form faces
        """
        loops = []
        visited = set()
        
        # Use a recursive helper function
        def find_loops_from_vertex(start, current, path, min_length=3):
            # If we're back at the start and path is long enough, we found a loop
            if current is start and len(path) >= min_length:
                loops.append(path.copy())
                return
                
            # Avoid revisiting vertices (except the start when the path is long enough)
            if current in visited and (current is not start or len(path) < min_length):
                return
                
            visited.add(current)
            
            # Try all connections from this vertex
            for next_vertex in current.connected_to:
                path.append(next_vertex)
                find_loops_from_vertex(start, next_vertex, path, min_length)
                path.pop()
                
            visited.remove(current)
            
        # Start from each vertex
        for vertex in self.vertices:
            find_loops_from_vertex(vertex, vertex, [vertex])
            
        return loops
        
    def generate_surfaces(self):
        """
        Generate surfaces from connected lines and explicit connections
        Uses advanced triangulation techniques
        """
        self.surfaces = []
        
        # First approach: use the existing line-to-line approach for backward compatibility
        self._generate_from_adjacent_lines()
        
        # Second approach: find closed loops and create faces
        self._generate_from_closed_loops()
        
        # Enhanced approach: Find nearby lines that could form surfaces
        self._generate_from_nearby_lines()
        
    def _generate_from_adjacent_lines(self):
        """Generate surfaces by connecting adjacent line segments"""
        # If fewer than 2 lines, no surfaces possible
        if len(self.lines) < 2:
            return
            
        # Get point lists from Line3D objects
        point_lines = []
        for line in self.lines:
            if len(line.vertices) >= 2:
                point_lines.append([v.position for v in line.vertices])
                
        # Basic surface generation by connecting adjacent lines
        for i in range(len(point_lines) - 1):
            line1 = point_lines[i]
            line2 = point_lines[i + 1]
            
            # Ensure lines have enough points
            if len(line1) < 2 or len(line2) < 2:
                continue
                
            # Create surfaces by connecting line points
            surface = []
            for j in range(min(len(line1), len(line2)) - 1):
                # Create two triangles to form a quad-like surface
                triangle1 = [
                    line1[j],
                    line1[j+1],
                    line2[j]
                ]
                
                triangle2 = [
                    line1[j+1],
                    line2[j+1],
                    line2[j]
                ]
                
                surface.extend([triangle1, triangle2])
                
            if surface:
                self.surfaces.append(surface)
    
    def _generate_from_nearby_lines(self):
        """Generate surfaces by connecting lines that are close to each other"""
        # Threshold distance for connecting lines
        connection_threshold = 1.0
        
        # Store lines that are already connected to avoid duplicate surfaces
        connected_pairs = set()
        
        # Check each pair of lines
        for i, line1 in enumerate(self.lines):
            if len(line1.vertices) < 2:
                continue
                
            for j, line2 in enumerate(self.lines):
                if i == j or (i, j) in connected_pairs or (j, i) in connected_pairs:
                    continue
                    
                if len(line2.vertices) < 2:
                    continue
                    
                # Check if lines are close enough
                min_distance = float('inf')
                for v1 in line1.vertices:
                    for v2 in line2.vertices:
                        dist = np.linalg.norm(np.array(v1.position) - np.array(v2.position))
                        min_distance = min(min_distance, dist)
                
                # If lines are close enough, connect them
                if min_distance <= connection_threshold:
                    surface = self._create_surface_between_lines(line1, line2)
                    if surface:
                        self.surfaces.append(surface)
                        connected_pairs.add((i, j))
    
    def _create_surface_between_lines(self, line1, line2):
        """
        Create a surface between two lines by connecting their vertices
        Returns a list of triangles forming the surface
        """
        # Extract vertex positions from both lines
        points1 = [v.position for v in line1.vertices]
        points2 = [v.position for v in line2.vertices]
        
        # Ensure there are enough points in each line
        if len(points1) < 2 or len(points2) < 2:
            return None
            
        # Create a surface by triangulating between the two lines
        surface = []
        
        # Determine which line has fewer points
        if len(points1) <= len(points2):
            shorter, longer = points1, points2
        else:
            shorter, longer = points2, points1
            
        # Calculate parameterization for the longer line
        params = np.linspace(0, 1, len(shorter))
        
        # Create triangles
        for i in range(len(shorter) - 1):
            # Get points on shorter line
            s1 = shorter[i]
            s2 = shorter[i + 1]
            
            # Get corresponding points on longer line
            idx1 = int(params[i] * (len(longer) - 1))
            idx2 = int(params[i + 1] * (len(longer) - 1))
            
            # Ensure indices are valid
            idx1 = max(0, min(idx1, len(longer) - 1))
            idx2 = max(0, min(idx2, len(longer) - 1))
            
            l1 = longer[idx1]
            l2 = longer[idx2]
            
            # Create two triangles (or one if points are the same)
            if idx1 != idx2:
                triangle1 = [s1, s2, l1]
                triangle2 = [s2, l2, l1]
                surface.extend([triangle1, triangle2])
            else:
                triangle = [s1, s2, l1]
                surface.append(triangle)
                
        return surface
                
    def _generate_from_closed_loops(self):
        """Generate surfaces from identified closed loops"""
        loops = self.find_closed_loops()
        
        for loop in loops:
            # Extract positions
            positions = [vertex.position for vertex in loop]
            
            if len(positions) < 3:
                continue
                
            # Use simple triangulation for convex faces (fan triangulation)
            surface = []
            for i in range(1, len(positions) - 1):
                triangle = [
                    positions[0],
                    positions[i],
                    positions[i+1]
                ]
                surface.append(triangle)
                
            if surface:
                self.surfaces.append(surface)
    
    def initialize_pottery(self):
        """
        Initialize a pottery mesh by creating a cylindrical shape
        
        This method is crucial for direct pottery sculpting mode:
        1. Creates an initial 3D mesh representing a cylinder
        2. Gives the pottery a slight hourglass shape for more natural look
        3. Prepares the mesh for deformation
        """
        # Reset pottery mesh
        self.pottery_mesh = []
        
        # Create a cylinder as the initial pottery shape
        vertices = []
        
        # Create vertices
        for i in range(self.pottery_rings):
            # Calculate Y coordinate, centered around zero
            y = -self.pottery_height/2 + i * (self.pottery_height / (self.pottery_rings - 1))
            
            # Use a slight hourglass shape for the initial pottery
            # This creates a more natural, tapering form
            radius_factor = 1.0 - 0.2 * math.sin(math.pi * i / (self.pottery_rings - 1))
            ring_radius = self.pottery_radius * radius_factor
            
            ring = []
            for j in range(self.pottery_slices):
                angle = 2.0 * math.pi * j / self.pottery_slices
                x = ring_radius * math.cos(angle)
                z = ring_radius * math.sin(angle)
                
                ring.append([x, y, z])
            
            vertices.append(ring)
        
        # Create triangular faces
        for i in range(self.pottery_rings - 1):
            for j in range(self.pottery_slices):
                j_next = (j + 1) % self.pottery_slices
                
                # Define the four corners of a quad on the cylinder
                v1 = vertices[i][j]
                v2 = vertices[i][j_next]
                v3 = vertices[i+1][j_next]
                v4 = vertices[i+1][j]
                
                # Create two triangles from the quad
                triangle1 = [v1, v2, v3]
                triangle2 = [v1, v3, v4]
                
                self.pottery_mesh.append([triangle1, triangle2])
        
        # Add to main surfaces for rendering
        self.surfaces = []  # Clear existing surfaces
        for mesh_triangles in self.pottery_mesh:
            self.surfaces.extend(mesh_triangles)
        
        return True
    
    def deform_pottery(self, point, radius=1.0, mode=0):
        """
        Deform the pottery mesh based on a 3D point
        - point: The 3D point to deform towards
        - radius: The radius of influence around the point
        - mode: 0 = add material (push outward), 1 = subtract material (push inward)
        """
        if not self.pottery_mesh:
            print("Initializing pottery mesh for sculpting")
            self.initialize_pottery()
        
        # Save the current sculpting mode    
        self.sculpt_mode = mode
            
        # Convert point to NumPy array for easy calculations
        try:
            deform_point = np.array(point)
            
            # Track whether any vertices were modified
            modified = False
            
            # Strength factors for each mode
            add_strength = 0.5      # Stronger for adding material
            subtract_strength = 0.7  # Stronger for subtracting
            
            # Go through all triangles in the pottery mesh
            for quad_triangles in self.pottery_mesh:
                for triangle in quad_triangles:
                    for i, vertex in enumerate(triangle):
                        # Convert to NumPy array
                        vertex_point = np.array(vertex)
                        
                        # Calculate distance to deformation point
                        distance = np.linalg.norm(vertex_point - deform_point)
                        
                        # Check if within radius of influence
                        if distance < radius:
                            # Calculate influence factor (closer points are affected more)
                            influence = (1.0 - distance / radius)
                            
                            if mode == 0:  # Add material (push outward from center)
                                try:
                                    # Calculate direction from center of pottery (approx at 0,0,0)
                                    # Keep y-coordinate the same to maintain the pottery's height profile
                                    center = np.array([0, vertex_point[1], 0])
                                    direction = vertex_point - center
                                    
                                    # Skip points too close to center
                                    if np.linalg.norm(direction) < 0.1:
                                        continue
                                    
                                    # Normalize direction vector - safely
                                    direction_length = np.linalg.norm(direction)
                                    if direction_length > 0.001:  # Avoid division by zero
                                        direction = direction / direction_length
                                        # Move vertex outward from center with stronger effect
                                        vertex_point = vertex_point + direction * influence * add_strength
                                    else:
                                        continue  # Skip this vertex if direction is near zero
                                
                                except Exception as e:
                                    print(f"Error in add deformation: {e}")
                                    continue  # Skip this vertex if there's an error
                                
                            else:  # Subtract material (push inward toward center)
                                try:
                                    # Calculate direction toward center of pottery
                                    center = np.array([0, vertex_point[1], 0])
                                    direction = center - vertex_point
                                    
                                    # Skip points too close to center
                                    if np.linalg.norm(direction) < 0.1:
                                        continue
                                    
                                    # Normalize direction - safely
                                    direction_length = np.linalg.norm(direction)
                                    if direction_length > 0.001:
                                        direction = direction / direction_length
                                    else:
                                        continue
                                    
                                    # Push vertex toward center based on distance from cursor
                                    push_direction = deform_point - vertex_point
                                    push_strength = influence * subtract_strength
                                    
                                    # Combined movement that pushes toward both cursor and center - safely
                                    if np.linalg.norm(push_direction) > 0.001:
                                        push_direction = push_direction / np.linalg.norm(push_direction)
                                        movement = (push_direction * 0.7 + direction * 0.3) * push_strength
                                        vertex_point = vertex_point + movement
                                
                                except Exception as e:
                                    print(f"Error in subtract deformation: {e}")
                                    continue  # Skip this vertex if there's an error
                            
                            # Update the vertex in the triangle
                            triangle[i] = vertex_point.tolist()
                            modified = True
            
            return modified
            
        except Exception as e:
            print(f"Error deforming pottery: {e}")
            return False
    
    def rotate_pottery(self, angle_degrees):
        """
        Rotate the pottery mesh around the Y axis
        - angle_degrees: Rotation angle in degrees
        """
        if not self.pottery_mesh:
            return False
            
        # Convert angle to radians
        angle = math.radians(angle_degrees)
        
        # Create rotation matrix around Y axis
        cos_a = math.cos(angle)
        sin_a = math.sin(angle)
        
        rotation_matrix = np.array([
            [cos_a, 0, sin_a],
            [0, 1, 0],
            [-sin_a, 0, cos_a]
        ])
        
        # Rotate all vertices in the pottery mesh
        for quad_triangles in self.pottery_mesh:
            for triangle in quad_triangles:
                for i, vertex in enumerate(triangle):
                    # Convert to NumPy array for matrix multiplication
                    vertex_point = np.array([vertex[0], vertex[1], vertex[2]])
                    
                    # Apply rotation
                    rotated_point = np.dot(rotation_matrix, vertex_point)
                    
                    # Update the vertex in the triangle
                    triangle[i] = rotated_point.tolist()
                    
        return True
    
    def start_profile_drawing(self, point):
        """Start drawing a new profile for pottery"""
        # Save the previous profile if it exists
        if len(self.profile_points) > 2:
            self.previous_profiles.append(self.profile_points.copy())
            if len(self.previous_profiles) > self.max_profiles:
                self.previous_profiles.pop(0)
        
        # Projection of the point to ensure it's on the positive X side
        profile_point = [abs(point[0]), point[1], 0]  # Keep only X positive for the profile
        
        # Reset the profile and start a new one
        self.profile_points = [profile_point]
        
        return True
    
    def add_profile_point(self, point):
        """Add a point to the current profile"""
        if not self.profile_points:
            return False
        
        # Projection of the point
        profile_point = [abs(point[0]), point[1], 0]  # Keep only X positive
        
        # Only add if it's far enough from the last point
        if np.linalg.norm(np.array(profile_point[:2]) - np.array(self.profile_points[-1][:2])) > 0.1:
            self.profile_points.append(profile_point)
            # Generate the surface immediately
            self.generate_pottery_surface()
            return True
        
        return False
    
    def generate_pottery_surface(self):
        """Generate a 3D surface by revolving the profile"""
        if len(self.profile_points) < 2:
            return False
        
        # Reset surface data
        self.pottery_surface_vertices = []
        self.pottery_surface_faces = []
        self.pottery_surface_normals = []
        self.surfaces = []  # Explicitly reset surfaces
        
        # Generate points by rotating around the Y axis
        for point_idx, point in enumerate(self.profile_points):
            x, y, _ = point
            
            # For each profile point, create a complete ring of points
            for segment in range(self.surface_segments):
                angle = 2.0 * math.pi * segment / self.surface_segments
                
                # 3D coordinates after rotation
                new_x = x * math.cos(angle)
                new_z = x * math.sin(angle)
                
                # Add the vertex
                self.pottery_surface_vertices.append([new_x, y, new_z])
                
                # Calculate approximate normal (pointing outward)
                normal_x = math.cos(angle)
...

This file has been truncated, please download it to see its full contents.

Repository

Credits

Auxence Daillen
1 project • 3 followers
Contact
Mohit Chopra
1 project • 3 followers
Contact

Comments

Please log in or sign up to comment.