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()