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.
Comments
Please log in or sign up to comment.