import cv2
import mediapipe as mp
import pygame
import sys
import traceback
import numpy as np
import math
import os
from datetime import datetime
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
from collections import deque
import copy
import threading
import trimesh # For 3D model loading
import tkinter as tk
from tkinter import filedialog
# Window dimensions
WIDTH, HEIGHT = 1280, 720
# MediaPipe configuration
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
# Gesture modes - kept from original for consistency
MODE_DRAWING = 0
MODE_ASSEMBLE_DISASSEMBLE = 1
# Sub-modes for assemble/disassemble
SUBMODE_SLICING = 0
SUBMODE_ASSEMBLY = 1
class UIOverlay:
"""Manages on-screen UI elements with validation message support"""
def __init__(self):
pygame.font.init()
self.font_large = pygame.font.SysFont('Arial', 20)
self.font_medium = pygame.font.SysFont('Arial', 16)
self.font_small = pygame.font.SysFont('Arial', 12)
self.texture_id = None
self.ui_surface = pygame.Surface((280, HEIGHT), pygame.SRCALPHA)
def create_texture(self):
"""Create an OpenGL texture from the pygame surface"""
if self.texture_id is None:
self.texture_id = glGenTextures(1)
texture_data = pygame.image.tostring(self.ui_surface, "RGBA", False)
glBindTexture(GL_TEXTURE_2D, self.texture_id)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.ui_surface.get_width(),
self.ui_surface.get_height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, texture_data)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
def draw_mode_instructions(self, mode, drawing_obj):
"""Draw mode-specific instructions with validation messages"""
x_offset = 10
y_offset = 10
line_height = 22
# Clear the surface
self.ui_surface.fill((0, 0, 0, 0))
# Mode indicator
mode_names = ["Drawing Mode", "Assemble/Disassemble"]
mode_colors = [(0, 255, 0), (255, 0, 0)]
# Draw semi-transparent background
bg_rect = pygame.Rect(0, 0, 280, HEIGHT)
pygame.draw.rect(self.ui_surface, (0, 0, 0, 180), bg_rect)
# Show validation message if present (priority display)
validation_msg = getattr(drawing_obj, 'validation_message', '')
if hasattr(drawing_obj, 'slicer') and hasattr(drawing_obj.slicer, 'validation_message'):
validation_msg = drawing_obj.slicer.validation_message
if validation_msg:
# Determine message color
if "" in validation_msg or "complete" in validation_msg.lower():
msg_color = (0, 255, 0) # Green for success
else:
msg_color = (255, 100, 100) # Red for warnings/errors
# Draw validation message prominently
msg_text = self.font_medium.render("VALIDATION:", True, (255, 255, 255))
self.ui_surface.blit(msg_text, (x_offset, y_offset))
y_offset += 25
# Word wrap the validation message
words = validation_msg.split()
lines = []
current_line = ""
for word in words:
test_line = current_line + " " + word if current_line else word
if len(test_line) > 35: # Approximate character limit per line
if current_line:
lines.append(current_line)
current_line = word
else:
current_line = test_line
if current_line:
lines.append(current_line)
for line in lines:
line_text = self.font_small.render(line, True, msg_color)
self.ui_surface.blit(line_text, (x_offset, y_offset))
y_offset += 18
y_offset += 15 # Extra spacing after validation message
# Draw mode header
mode_text = self.font_large.render(mode_names[mode], True, mode_colors[mode])
self.ui_surface.blit(mode_text, (x_offset, y_offset))
y_offset += 35
# Draw mode-specific instructions
instructions = []
if mode == MODE_DRAWING:
instructions = [
"Right Hand:",
" Pinch: Draw lines",
" Hover: Preview points",
"",
"Left Hand:",
" Pinch: Navigate cam",
"",
"Keyboard:",
" H: Create solid chair",
" D: Create solid box",
" L: Load solid model",
" U: Dismantle into parts",
" G: Random compilation",
" F: Reset to original",
" Shift: Switch mode",
" C: Clear drawing",
" X: Clear current view",
" Tab: Toggle grid",
" +/-: Zoom in/out",
" K: Save scene as OBJ",
" Space: Reset all"
]
elif mode == MODE_ASSEMBLE_DISASSEMBLE:
if drawing_obj.submode == SUBMODE_SLICING:
plane_orient = "Horizontal" if drawing_obj.slicer.slice_plane.is_horizontal else "Vertical"
selected_model_info = "Selected" if drawing_obj.slicer.selected_for_slicing else "None"
hovered_model_info = "Yes" if drawing_obj.slicer.hovered_model else "No"
instructions = [
"SLICING MODE",
" Select specific piece to cut",
"",
"Model Selection:",
" Hover over model to target",
" S: Select hovered model",
" Only selected model gets cut!",
"",
"Plane Control:",
" Pinch: Move plane",
" P: Toggle control",
" (position/rotation)",
" O: Toggle orientation",
f" ({plane_orient})",
" +/-: Scale plane",
f" (Size: {drawing_obj.slicer.slice_plane.size:.1f})",
" E: Execute slice",
" (cuts selected model only)",
" Z: Center plane on selected",
"",
"Status:",
f" Selected model: {selected_model_info}",
f" Model under cursor: {hovered_model_info}",
f" Total models: {len(drawing_obj.slicer.models)}",
"",
"Features:",
" Realistic woodworking cuts",
" Only cuts selected piece",
" Other pieces stay intact",
" Precise geometry cutting",
"",
"Keyboard:",
" L: Load solid model",
" H: Create solid chair",
" D: Create solid box",
" U: Dismantle into parts",
" G: Random compilation",
" F: Reset to original",
" M: Switch to assembly",
" Shift: Back to drawing",
" R: Reset plane",
" K: Save scene as OBJ",
" X: Clear current view",
" Space: Reset all",
"",
f"Control: {drawing_obj.slice_control_mode}"
]
else: # SUBMODE_ASSEMBLY
instructions = [
"ASSEMBLY MODE",
"",
"Right Hand:",
" Pinch/Grab: Select",
f" Mode: {drawing_obj.assembly_mode.upper()}",
" Fist: Delete model",
"",
"Left Hand:",
" Pinch: Navigate",
"",
"Keyboard:",
" M: Switch to slicing",
" Shift: Back to drawing",
" A: Toggle move/rotate",
" R: Reset positions",
" T: Show/hide trash bin",
" Q: Restore deleted model",
" N: Toggle snap mode",
f" (Snap: {'ON' if drawing_obj.snap_mode else 'OFF'})",
" U: Dismantle into parts",
" G: Random compilation",
" F: Reset to original",
" K: Save scene as OBJ",
" X: Clear current view",
" Space: Reset all",
" +/-: Zoom",
"",
"Current model:",
f" {drawing_obj.selected_model is not None}",
"",
"Trash items: ",
f" {len(drawing_obj.trash_bin.deleted_models) if hasattr(drawing_obj, 'trash_bin') else 0}"
]
# Draw instructions
for instruction in instructions:
if instruction.startswith(" "):
text_surf = self.font_small.render(instruction, True, (200, 200, 200))
elif instruction.endswith(":") or instruction in ["SLICING MODE", "ASSEMBLY MODE"]:
text_surf = self.font_medium.render(instruction, True, (255, 255, 255))
elif instruction.startswith(""):
text_surf = self.font_small.render(instruction, True, (255, 200, 100))
elif instruction.startswith("Features:") or instruction.startswith("Status:"):
text_surf = self.font_small.render(instruction, True, (255, 255, 100))
else:
text_surf = self.font_small.render(instruction, True, (180, 180, 180))
self.ui_surface.blit(text_surf, (x_offset, y_offset))
y_offset += line_height
# Create/update OpenGL texture
self.create_texture()
def render_to_opengl(self):
"""Render the UI texture to the OpenGL screen"""
if self.texture_id is None:
return
# Save OpenGL state
glPushAttrib(GL_ALL_ATTRIB_BITS)
# Set up 2D rendering
glMatrixMode(GL_PROJECTION)
glPushMatrix()
glLoadIdentity()
glOrtho(0, WIDTH, HEIGHT, 0, -1, 1)
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glLoadIdentity()
# Enable texturing and blending
glEnable(GL_TEXTURE_2D)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glDisable(GL_DEPTH_TEST)
glDisable(GL_LIGHTING)
# Bind texture and draw quad
glBindTexture(GL_TEXTURE_2D, self.texture_id)
glColor4f(1, 1, 1, 1)
glBegin(GL_QUADS)
glTexCoord2f(0, 0); glVertex2f(0, 0)
glTexCoord2f(1, 0); glVertex2f(280, 0)
glTexCoord2f(1, 1); glVertex2f(280, HEIGHT)
glTexCoord2f(0, 1); glVertex2f(0, HEIGHT)
glEnd()
# Restore OpenGL state
glPopMatrix()
glMatrixMode(GL_PROJECTION)
glPopMatrix()
glPopAttrib()
class TrashBin:
"""Enhanced trash bin with improved gesture detection and recovery"""
def __init__(self):
self.position = [WIDTH - 100, HEIGHT - 100] # Bottom right corner
self.size = 80
self.texture_id = None
self.ui_surface = pygame.Surface((self.size, self.size), pygame.SRCALPHA)
self.is_hovered = False
self.attraction_radius = 120
self.attraction_strength = 0.08
pygame.font.init()
self.font = pygame.font.SysFont('Arial', 12)
self.deleted_models = []
self.deletion_candidates = set()
self.visible = True
def update(self, mouse_pos):
"""Update hover state based on mouse position"""
if not self.visible:
return
self.is_hovered = (
mouse_pos[0] >= self.position[0] - self.size/2 and
mouse_pos[0] <= self.position[0] + self.size/2 and
mouse_pos[1] >= self.position[1] - self.size/2 and
mouse_pos[1] <= self.position[1] + self.size/2
)
def create_texture(self):
"""Create an OpenGL texture from the pygame surface"""
if not self.visible:
return
if self.texture_id is None:
self.texture_id = glGenTextures(1)
# Draw the trash bin icon
self.ui_surface.fill((0, 0, 0, 0))
# Draw bin body with better visual feedback
if self.deletion_candidates:
color = (255, 100, 100) if self.is_hovered else (200, 80, 80)
else:
color = (150, 50, 50) if self.is_hovered else (100, 30, 30)
pygame.draw.rect(self.ui_surface, color, (10, 20, self.size - 20, self.size - 30))
# Draw bin top
pygame.draw.rect(self.ui_surface, (120, 120, 120), (5, 10, self.size - 10, 10))
# Add trash label
label = self.font.render("TRASH", True, (255, 255, 255))
label_rect = label.get_rect(center=(self.size/2, self.size/2 - 5))
self.ui_surface.blit(label, label_rect)
# Show count of deleted items
if self.deleted_models:
count_label = self.font.render(f"({len(self.deleted_models)})", True, (255, 255, 0))
count_rect = count_label.get_rect(center=(self.size/2, self.size/2 + 15))
self.ui_surface.blit(count_label, count_rect)
# If there are deletion candidates, animate the bin
if self.deletion_candidates:
# Draw pulsing outline
pulse = int(255 * (0.7 + 0.3 * math.sin(pygame.time.get_ticks() * 0.02)))
outline_color = (pulse, 50, 50, 200)
pygame.draw.rect(self.ui_surface, outline_color, (0, 0, self.size, self.size), 4)
texture_data = pygame.image.tostring(self.ui_surface, "RGBA", False)
glBindTexture(GL_TEXTURE_2D, self.texture_id)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.ui_surface.get_width(),
self.ui_surface.get_height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, texture_data)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
def render_to_opengl(self):
"""Render the trash bin to the OpenGL screen"""
if not self.visible:
return
self.create_texture()
if self.texture_id is None:
return
# Save OpenGL state
glPushAttrib(GL_ALL_ATTRIB_BITS)
# Set up 2D rendering
glMatrixMode(GL_PROJECTION)
glPushMatrix()
glLoadIdentity()
glOrtho(0, WIDTH, HEIGHT, 0, -1, 1)
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glLoadIdentity()
# Enable texturing and blending
glEnable(GL_TEXTURE_2D)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glDisable(GL_DEPTH_TEST)
glDisable(GL_LIGHTING)
# Bind texture and draw quad
glBindTexture(GL_TEXTURE_2D, self.texture_id)
glColor4f(1, 1, 1, 1)
# Draw at position
x = self.position[0] - self.size/2
y = self.position[1] - self.size/2
glBegin(GL_QUADS)
glTexCoord2f(0, 0); glVertex2f(x, y)
glTexCoord2f(1, 0); glVertex2f(x + self.size, y)
glTexCoord2f(1, 1); glVertex2f(x + self.size, y + self.size)
glTexCoord2f(0, 1); glVertex2f(x, y + self.size)
glEnd()
# Restore OpenGL state
glPopMatrix()
glMatrixMode(GL_PROJECTION)
glPopMatrix()
glPopAttrib()
def is_model_over_trash(self, model, screen_pos):
"""Enhanced check if a model is over or near the trash bin"""
if not self.visible:
return 0.0
# Calculate distance to trash bin
distance = math.sqrt(
(screen_pos[0] - self.position[0])**2 +
(screen_pos[1] - self.position[1])**2
)
# Direct overlap check
if distance < self.size/2:
self.deletion_candidates.add(model)
return 1.0
# Attraction radius check
if distance < self.attraction_radius:
self.deletion_candidates.add(model)
strength = 1.0 - (distance / self.attraction_radius)
return strength * self.attraction_strength
# Not a deletion candidate anymore
if model in self.deletion_candidates:
self.deletion_candidates.remove(model)
return 0.0
def update_attraction(self, model, screen_pos):
"""Apply attraction force to models near trash bin"""
if not self.visible:
return [0, 0]
attraction_strength = self.is_model_over_trash(model, screen_pos)
if attraction_strength > 0:
# Calculate direction to trash bin
dx = self.position[0] - screen_pos[0]
dy = self.position[1] - screen_pos[1]
# Normalize and scale by attraction strength
length = math.sqrt(dx*dx + dy*dy)
if length > 0:
dx = dx / length * attraction_strength * 8
dy = dy / length * attraction_strength * 8
return [dx, dy]
return [0, 0]
def delete_model(self, model):
"""Delete a model and store it for potential recovery"""
# Store the model in the deleted list with timestamp
model_data = {
'model': model,
'timestamp': pygame.time.get_ticks(),
'original_position': model.position.copy(),
'original_rotation': model.rotation.copy()
}
self.deleted_models.append(model_data)
# Limit the number of deleted models stored
if len(self.deleted_models) > 20:
self.deleted_models.pop(0) # Remove oldest
# Remove from candidates
if model in self.deletion_candidates:
self.deletion_candidates.remove(model)
def recover_last_model(self):
"""Recover the most recently deleted model"""
if self.deleted_models:
model_data = self.deleted_models.pop()
model = model_data['model']
# Reset model properties
model.position = model_data['original_position']
model.rotation = model_data['original_rotation']
model.deletion_candidate = False
model.deletion_progress = 0.0
model.is_grabbed = False
# Reset colors
model.color = model.orig_color.copy()
return model
return None
def toggle_visibility(self):
"""Toggle the trash bin visibility"""
self.visible = not self.visible
return self.visible
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)
self.weights = self.weights / np.sum(self.weights)
def update(self, new_point):
if new_point is None:
return None
try:
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:
return new_point
self.points_buffer.append(point)
if len(self.points_buffer) < 2:
return point.tolist()
point_array = np.array(list(self.points_buffer))
weights = self.weights[-len(self.points_buffer):]
weights = weights / np.sum(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):
self.points_buffer.clear()
class AirCanvasCamera:
"""Camera control system"""
def __init__(self):
self.position = np.array([0.0, 0.0, -10.0], dtype=float)
self.rotation_x = 0.0
self.rotation_y = 0.0
self.zoom = 1.0
self.is_navigating = False
self.last_nav_point = None
self.rotation_sensitivity = 20.0
self.movement_sensitivity = 5.0
self.smoother = SmoothingFilter(window_size=15)
def move(self, dx, dy, dz):
try:
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):
try:
damping = 0.5
self.rotation_y += dx * self.rotation_sensitivity * damping
self.rotation_x += dy * self.rotation_sensitivity * damping
self.rotation_x = max(-90, min(90, self.rotation_x))
except Exception as e:
print(f"Camera rotation error: {e}")
def zoom_in(self):
self.zoom *= 1.1
self.position[2] = -10.0 / self.zoom
def zoom_out(self):
self.zoom *= 0.9
self.position[2] = -10.0 / self.zoom
def reset(self):
self.position = np.array([0.0, 0.0, -10.0], dtype=float)
self.rotation_x = 0.0
self.rotation_y = 0.0
self.zoom = 1.0
def start_navigation(self, hand_point):
try:
self.is_navigating = True
self.last_nav_point = hand_point
self.smoother.reset()
except Exception as e:
print(f"Navigation start error: {e}")
def navigate(self, hand_point):
try:
if self.is_navigating and self.last_nav_point:
smoothed_point = self.smoother.update(hand_point)
if smoothed_point and hasattr(smoothed_point, 'x'):
dx = smoothed_point.x - self.last_nav_point.x
dy = smoothed_point.y - self.last_nav_point.y
self.rotate(dx * 100, dy * 100)
self.last_nav_point = smoothed_point
elif isinstance(smoothed_point, (list, np.ndarray)) and len(smoothed_point) >= 2:
dx = smoothed_point[0] - self.last_nav_point.x
dy = smoothed_point[1] - self.last_nav_point.y
self.rotate(dx * 100, dy * 100)
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):
self.is_navigating = False
self.last_nav_point = None
class SlicePlane:
"""Enhanced slicing plane with full control from original"""
def __init__(self):
self.origin = np.array([0.0, 0.0, 0.0], dtype=float)
self.normal = np.array([0.0, 1.0, 0.0], dtype=float) # Default horizontal
self.size = 5.0
self.visible = True
self.active = False
self.rotation_x = 0.0
self.rotation_y = 0.0
self.last_position = None
self.is_controlled = False
self.is_horizontal = True # True = horizontal, False = vertical
# Control constraints
self.object_bounds = None
self.constraint_enabled = True
self.control_sensitivity = 0.5
def toggle_orientation(self):
"""Toggle between horizontal and vertical orientation"""
self.is_horizontal = not self.is_horizontal
if self.is_horizontal:
# Horizontal plane (perpendicular to Y axis)
self.normal = np.array([0.0, 1.0, 0.0])
self.rotation_x = 0.0
self.rotation_y = 0.0
else:
# Vertical plane (perpendicular to X axis by default)
self.normal = np.array([1.0, 0.0, 0.0])
self.rotation_x = 0.0
self.rotation_y = 0.0
# Re-center after orientation change if we have object bounds
if self.object_bounds is not None:
self.center_on_object(self.object_bounds)
def scale_up(self):
"""Increase the size of the plane"""
self.size = min(self.size * 1.2, 20.0)
def scale_down(self):
"""Decrease the size of the plane"""
self.size = max(self.size * 0.8, 1.0)
def set_position(self, position):
"""Set position with optional constraint to object bounds"""
new_pos = np.array(position, dtype=float)
# Apply constraints if enabled and bounds are set
if self.constraint_enabled and self.object_bounds is not None:
min_bounds, max_bounds = self.object_bounds
# Add padding based on plane size
padding = self.size * 0.5
constrained_min = min_bounds - padding
constrained_max = max_bounds + padding
# Constrain position to expanded object bounds
new_pos = np.clip(new_pos, constrained_min, constrained_max)
self.origin = new_pos
def set_rotation(self, rot_x, rot_y):
"""Set rotation with angle limits to avoid extreme angles"""
# Limit rotation angles for better usability
self.rotation_x = np.clip(rot_x, -80, 80)
self.rotation_y = np.clip(rot_y, -80, 80)
rad_x = np.radians(self.rotation_x)
rad_y = np.radians(self.rotation_y)
# Get base normal based on orientation
if self.is_horizontal:
base_normal = np.array([0, 1, 0])
else:
base_normal = np.array([1, 0, 0])
Ry = np.array([
[np.cos(rad_y), 0, np.sin(rad_y)],
[0, 1, 0],
[-np.sin(rad_y), 0, np.cos(rad_y)]
])
Rx = np.array([
[1, 0, 0],
[0, np.cos(rad_x), -np.sin(rad_x)],
[0, np.sin(rad_x), np.cos(rad_x)]
])
self.normal = np.dot(Ry, np.dot(Rx, base_normal))
def set_object_bounds(self, bounds):
"""Set the object bounds for constraining plane movement"""
self.object_bounds = bounds
def center_on_object(self, bounds=None):
"""Center the slice plane on the object's center"""
if bounds:
self.object_bounds = bounds
if self.object_bounds:
min_bounds, max_bounds = self.object_bounds
center = (min_bounds + max_bounds) / 2.0
# For both orientations, use the full center
self.origin = np.array([center[0], center[1], center[2]], dtype=float)
# Reset rotations when centering
self.rotation_x = 0.0
self.rotation_y = 0.0
self.set_rotation(self.rotation_x, self.rotation_y)
# Adjust plane size based on object bounds
size_x = max_bounds[0] - min_bounds[0]
size_y = max_bounds[1] - min_bounds[1]
size_z = max_bounds[2] - min_bounds[2]
max_size = max(size_x, size_y, size_z) * 1.2 # 20% larger than object
# Set plane size based on object size
self.size = min(max(max_size, 3.0), 15.0) # Constrain between 3 and 15
return True
return False
def control_with_hand(self, hand_position, mode="position"):
"""Control plane with improved sensitivity and constraints"""
if mode == "position":
if self.last_position is not None:
# Calculate delta with reduced sensitivity
delta = (np.array(hand_position) - self.last_position) * self.control_sensitivity
# Apply smoother movement with dynamically adjusted sensitivity
if self.object_bounds is not None:
min_bounds, max_bounds = self.object_bounds
center = (min_bounds + max_bounds) / 2.0
# Distance from object center affects sensitivity
curr_dist = np.linalg.norm(self.origin - center)
object_size = np.linalg.norm(max_bounds - min_bounds) * 0.5
# Reduce sensitivity as we move away from center
if curr_dist > object_size:
distance_factor = max(0.2, 1.0 - (curr_dist - object_size) / object_size)
delta *= distance_factor
# Apply movement
self.set_position(self.origin + delta)
self.last_position = np.array(hand_position)
elif mode == "rotation":
if self.last_position is not None:
delta = np.array(hand_position) - self.last_position
# Adjust rotation sensitivity
rotation_sensitivity = self.control_sensitivity * 80.0
# Update rotation
new_rot_x = self.rotation_x + delta[1] * rotation_sensitivity
new_rot_y = self.rotation_y + delta[0] * rotation_sensitivity
self.set_rotation(new_rot_x, new_rot_y)
self.last_position = np.array(hand_position)
def stop_control(self):
self.last_position = None
self.is_controlled = False
def reset(self):
"""Reset plane with optional automatic centering"""
if self.object_bounds:
self.center_on_object()
else:
self.origin = np.array([0.0, 0.0, 0.0], dtype=float)
self.rotation_x = 0.0
self.rotation_y = 0.0
if self.is_horizontal:
self.normal = np.array([0.0, 1.0, 0.0])
else:
self.normal = np.array([1.0, 0.0, 0.0])
self.size = 5.0
self.visible = True
def draw(self):
if not self.visible:
return
glPushMatrix()
glTranslatef(*self.origin)
# Apply rotation
glRotatef(self.rotation_y, 0, 1, 0)
glRotatef(self.rotation_x, 1, 0, 0)
# Apply base orientation
if not self.is_horizontal:
glRotatef(90, 0, 1, 0) # Rotate to vertical
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
# Draw plane
glBegin(GL_QUADS)
if self.active:
glColor4f(1.0, 0.0, 0.0, 0.4)
else:
glColor4f(0.8, 0.8, 0.0, 0.3)
# Draw horizontal plane in XZ
glVertex3f(-self.size, 0, -self.size)
glVertex3f(self.size, 0, -self.size)
glVertex3f(self.size, 0, self.size)
glVertex3f(-self.size, 0, self.size)
glEnd()
# Draw border
glLineWidth(2.0)
glBegin(GL_LINE_LOOP)
glColor3f(1.0, 1.0, 0.0)
glVertex3f(-self.size, 0, -self.size)
glVertex3f(self.size, 0, -self.size)
glVertex3f(self.size, 0, self.size)
glVertex3f(-self.size, 0, self.size)
glEnd()
# Draw normal direction indicator
glBegin(GL_LINES)
glColor3f(1.0, 0.0, 0.0)
glVertex3f(0, 0, 0)
glVertex3f(0, self.size/2, 0)
glEnd()
# Draw crosshair to indicate center
crosshair_size = self.size * 0.1
glBegin(GL_LINES)
glColor3f(1.0, 1.0, 1.0)
glVertex3f(-crosshair_size, 0, 0)
glVertex3f(crosshair_size, 0, 0)
glVertex3f(0, 0, -crosshair_size)
glVertex3f(0, 0, crosshair_size)
glEnd()
glDisable(GL_BLEND)
glPopMatrix()
class ModelMesh:
"""Enhanced ModelMesh with robust slicing - fixed for woodworking precision"""
def __init__(self, vertices=None, faces=None, normals=None, color=None):
self.vertices = np.array(vertices) if vertices is not None else np.empty((0, 3))
self.faces = faces if faces is not None else []
self.face_normals = normals if normals is not None else []
self.color = color or [0.5, 0.7, 0.8, 1.0]
self.orig_color = self.color.copy()
# Position and orientation
self.position = np.array([0.0, 0.0, 0.0], dtype=float)
self.rotation = np.array([0.0, 0.0, 0.0], dtype=float)
self.scale = 1.0
# Selection and hover properties
self.selected = False
self.hovered = False
self.is_grabbed = False
# Trash bin interaction properties
self.deletion_candidate = False
self.deletion_progress = 0.0
# Physics properties for smooth movement
self.velocity = np.array([0.0, 0.0, 0.0], dtype=float)
self.angular_velocity = np.array([0.0, 0.0, 0.0], dtype=float)
self.damping = 0.8
# Compute derived properties
self._compute_face_normals()
self._compute_bounds()
def _compute_face_normals(self):
"""Compute normal vectors for all faces"""
self.face_normals = []
for face in self.faces:
if len(face) >= 3:
v0 = self.vertices[face[0]]
v1 = self.vertices[face[1]]
v2 = self.vertices[face[2]]
# Calculate face normal using cross product
normal = np.cross(v1 - v0, v2 - v0)
norm = np.linalg.norm(normal)
if norm > 0.00001:
normal = normal / norm
else:
normal = np.array([0.0, 1.0, 0.0]) # Default normal if degenerate triangle
self.face_normals.append(normal)
else:
# Default normal for invalid faces
self.face_normals.append(np.array([0.0, 1.0, 0.0]))
def _compute_bounds(self):
"""Compute the bounding box of the mesh"""
if len(self.vertices) == 0:
self.center = np.array([0.0, 0.0, 0.0])
self.bounding_box = (
np.array([-1.0, -1.0, -1.0]),
np.array([1.0, 1.0, 1.0])
)
return
# Compute bounding box
min_bounds = np.min(self.vertices, axis=0)
max_bounds = np.max(self.vertices, axis=0)
self.bounding_box = (min_bounds, max_bounds)
self.center = (min_bounds + max_bounds) / 2.0
def get_world_vertices(self):
"""Get vertices in world coordinates with transformation applied"""
if len(self.vertices) == 0:
return np.empty((0, 3))
# Apply scale
scaled_vertices = self.vertices * self.scale
# Apply rotation (simplified for small angles)
if np.any(self.rotation != 0):
rx, ry, rz = np.radians(self.rotation)
# Rotation matrices
Rx = np.array([
[1, 0, 0],
[0, np.cos(rx), -np.sin(rx)],
[0, np.sin(rx), np.cos(rx)]
])
Ry = np.array([
[np.cos(ry), 0, np.sin(ry)],
[0, 1, 0],
[-np.sin(ry), 0, np.cos(ry)]
])
Rz = np.array([
[np.cos(rz), -np.sin(rz), 0],
[np.sin(rz), np.cos(rz), 0],
[0, 0, 1]
])
# Combined rotation
R = np.dot(Rz, np.dot(Ry, Rx))
# Apply rotation to all vertices
rotated_vertices = np.dot(scaled_vertices, R.T)
else:
rotated_vertices = scaled_vertices
# Apply translation
world_vertices = rotated_vertices + self.position
return world_vertices
def update_hover(self, cursor_position, threshold=3.0):
"""Check if cursor is hovering over this mesh"""
...
This file has been truncated, please download it to see its full contents.
Comments