import streamlit as st import trimesh import numpy as np import io import os from datetime import datetime def setup_page(): """Настройка страницы""" st.set_page_config( page_title="3D Model Repair Tool", page_icon="🔄", layout="wide" ) st.title("🔄 3D Model Repair Tool") st.markdown(""" Загрузите одну или несколько 3D моделей для объединения и лечения. Поддерживаемые форматы: STL, OBJ, PLY, GLTF, GLB """) def load_and_validate_mesh(uploaded_file): """Загрузка и валидация меша""" try: # Сохраняем временный файл with open(f"temp_{uploaded_file.name}", "wb") as f: f.write(uploaded_file.getbuffer()) # Загружаем меш mesh = trimesh.load(f"temp_{uploaded_file.name}") # Очищаем временный файл os.remove(f"temp_{uploaded_file.name}") if isinstance(mesh, trimesh.Scene): mesh = mesh.dump().sum() return mesh, None except Exception as e: return None, f"Ошибка загрузки {uploaded_file.name}: {str(e)}" def repair_mesh(mesh): """Лечение меша""" try: # Проверяем и исправляем меш if not mesh.is_watertight: # Заполняем отверстия mesh.fill_holes() # Убираем вырожденные треугольники mesh.remove_duplicate_faces() mesh.remove_degenerate_faces() # Убираем бесконечно малые компоненты if len(mesh.split()) > 1: meshes = mesh.split() # Выбираем самый большой компонент main_mesh = max(meshes, key=lambda x: x.vertices.shape[0]) mesh = main_mesh return mesh, None except Exception as e: return None, f"Ошибка лечения: {str(e)}" def merge_meshes(meshes): """Объединение мешей""" try: if len(meshes) == 1: return meshes[0], None # Объединяем все меши combined = trimesh.util.concatenate(meshes) return combined, None except Exception as e: return None, f"Ошибка объединения: {str(e)}" def create_download_link(mesh, filename, file_format): """Создание ссылки для скачивания""" try: # Экспортируем в байты export_bytes = mesh.export(file_type=file_format) # Создаем кнопку для скачивания st.download_button( label=f"📥 Скачать {filename}.{file_format}", data=export_bytes, file_name=f"{filename}.{file_format}", mime="application/octet-stream" ) return True except Exception as e: st.error(f"Ошибка создания файла: {str(e)}") return False def show_mesh_info(mesh, name): """Отображение информации о меше""" col1, col2, col3, col4 = st.columns(4) with col1: st.metric(f"Вершины {name}", f"{len(mesh.vertices):,}") with col2: st.metric(f"Полигоны {name}", f"{len(mesh.faces):,}") with col3: watertight = "✅" if mesh.is_watertight else "❌" st.metric(f"Водонепроницаемость {name}", watertight) with col4: volume = f"{mesh.volume:.2f}" if mesh.is_watertight else "N/A" st.metric(f"Объем {name}", volume) def main(): setup_page() # Загрузка файлов uploaded_files = st.file_uploader( "Выберите 3D файлы", type=['stl', 'obj', 'ply', 'gltf', 'glb'], accept_multiple_files=True, help="Можно выбрать несколько файлов для объединения" ) if not uploaded_files: st.info("👆 Загрузите один или несколько 3D файлов для начала") return # Настройки обработки st.sidebar.header("⚙️ Настройки обработки") repair_option = st.sidebar.checkbox("Автоматическое лечение моделей", value=True) remove_small_parts = st.sidebar.checkbox("Удалить мелкие компоненты", value=True) output_format = st.sidebar.selectbox( "Формат результата", ['stl', 'obj', 'ply'] ) # Обработка файлов if st.button("🔄 Обработать модели", type="primary"): with st.spinner("Обработка моделей..."): meshes = [] errors = [] # Загрузка и валидация всех мешей for i, uploaded_file in enumerate(uploaded_files): with st.expander(f"📄 {uploaded_file.name}", expanded=False): mesh, error = load_and_validate_mesh(uploaded_file) if error: errors.append(error) st.error(error) continue # Показываем информацию о исходной модели show_mesh_info(mesh, "исходная") # Лечение меша если нужно if repair_option: mesh, repair_error = repair_mesh(mesh) if repair_error: errors.append(repair_error) st.error(repair_error) continue meshes.append(mesh) st.success(f"✅ Модель {uploaded_file.name} успешно обработана") if errors: st.error(f"Найдено ошибок: {len(errors)}") for error in errors: st.error(error) if not meshes: st.error("❌ Не удалось обработать ни одну модель") return # Объединение мешей if len(meshes) > 1: st.subheader("🔗 Объединение моделей") combined_mesh, merge_error = merge_meshes(meshes) if merge_error: st.error(merge_error) return st.success(f"✅ Успешно объединено {len(meshes)} моделей") else: combined_mesh = meshes[0] # Финальная обработка if remove_small_parts and len(combined_mesh.split()) > 1: meshes_split = combined_mesh.split() if len(meshes_split) > 1: # Оставляем только самый большой компонент main_mesh = max(meshes_split, key=lambda x: x.vertices.shape[0]) combined_mesh = main_mesh st.info(f"Удалены мелкие компоненты, оставлен основной меш") # Показываем информацию о результате st.subheader("📊 Результат обработки") show_mesh_info(combined_mesh, "результат") # Визуализация (опционально) if st.checkbox("Показать 3D предпросмотр"): try: # Создаем временный файл для предпросмотра combined_mesh.export("preview.obj") st.components.v1.html(f""" """, height=500) except: st.warning("Предпросмотр 3D временно недоступен") # Секция скачивания st.subheader("📥 Скачать результат") # Генерируем имя файла timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") if len(uploaded_files) == 1: base_name = os.path.splitext(uploaded_files[0].name)[0] filename = f"{base_name}_repaired_{timestamp}" else: filename = f"combined_models_{timestamp}" # Создаем ссылки для скачивания в разных форматах col1, col2, col3 = st.columns(3) with col1: create_download_link(combined_mesh, filename, output_format) with col2: create_download_link(combined_mesh, filename, 'stl') with col3: create_download_link(combined_mesh, filename, 'obj') st.success("✅ Обработка завершена! Вы можете скачать результат выше") if __name__ == "__main__": main()