# 3D Print Calculator — Техническое задание на MVP ## Обзор проекта Сервис 3D-печати на заказ с автоматическим расчётом стоимости. Клиент загружает 3D-модель (STL, 3MF, OBJ), выбирает материал, получает мгновенный расчёт цены и оформляет заказ. AI-ассистент помогает выбрать оптимальный материал под задачу. **Бизнес-модель:** B2B — корпуса для электроники, функциональные запчасти, прототипы. **Стек:** Python (FastAPI) + Vue 3 (Vite) + PostgreSQL + Nginx + Docker Compose. **Хостинг:** VPS (Ubuntu 24), деплой через Docker Compose. --- ## Архитектура ``` ┌─────────────────────────────────────────────┐ │ Nginx (reverse proxy) │ │ :80 → frontend, /api → backend │ └──────────┬──────────────┬───────────────────┘ │ │ ┌──────────▼──────┐ ┌─────▼──────────────────┐ │ Vue 3 (Vite) │ │ FastAPI (Python) │ │ SPA, static │ │ REST API + WebSocket │ │ Port: 5173 │ │ Port: 8000 │ └─────────────────┘ └──────┬─────┬────────────┘ │ │ ┌──────▼┐ ┌──▼───────────┐ │ PostgreSQL │ File Storage │ │ :5432 │ (local /uploads) │ └───────┘ └──────────────┘ ``` ### Контейнеры Docker Compose 1. **frontend** — Node 20 + Vite dev server (в проде — собранная статика через Nginx) 2. **backend** — Python 3.12 + FastAPI + Uvicorn 3. **db** — PostgreSQL 16 4. **nginx** — reverse proxy, SSL termination --- ## Backend (FastAPI) ### Структура проекта ``` backend/ ├── app/ │ ├── main.py # FastAPI app, CORS, middleware │ ├── config.py # Settings (pydantic-settings) │ ├── database.py # SQLAlchemy async engine + session │ ├── models/ │ │ ├── material.py # Material ORM model │ │ ├── order.py # Order ORM model │ │ └── file_upload.py # Uploaded file metadata │ ├── schemas/ │ │ ├── calculate.py # Request/Response для калькулятора │ │ ├── material.py # Material schemas │ │ └── order.py # Order schemas │ ├── routers/ │ │ ├── calculate.py # POST /api/calculate │ │ ├── materials.py # GET /api/materials │ │ ├── orders.py # POST /api/orders │ │ └── ai_advisor.py # POST /api/advisor │ ├── services/ │ │ ├── file_parser.py # Парсинг STL/3MF/OBJ → геометрия │ │ ├── price_engine.py # Расчёт стоимости │ │ ├── ai_advisor.py # Интеграция с Claude API │ │ └── telegram_notify.py # Уведомления в Telegram │ └── seed/ │ └── materials.py # Начальные данные по материалам ├── requirements.txt ├── Dockerfile └── alembic/ # Миграции БД ``` ### API Endpoints #### 1. POST /api/calculate Основной endpoint калькулятора. Принимает файл и параметры, возвращает расчёт. **Request:** `multipart/form-data` - `file` (File, required) — 3D-модель (STL, 3MF, OBJ). Макс. размер: 50MB. - `material_id` (int, required) — ID выбранного материала - `infill_percent` (int, optional, default=30) — Процент заполнения (10-100) - `layer_height_mm` (float, optional, default=0.2) — Высота слоя (0.08-0.4) - `quantity` (int, optional, default=1) — Количество экземпляров (1-500) - `post_processing` (str[], optional) — Постобработка: ["sanding", "painting", "threading"] **Response:** `application/json` ```json { "success": true, "file_info": { "filename": "case_v3.stl", "format": "stl", "volume_cm3": 42.7, "surface_area_cm2": 198.3, "bounding_box_mm": {"x": 120.0, "y": 80.0, "z": 35.0}, "is_watertight": true, "triangle_count": 12840 }, "calculation": { "material": { "id": 3, "name": "PA (Nylon)", "density_g_cm3": 1.14, "price_per_gram": 50.0 }, "weight_grams": 48.7, "material_cost_rub": 2435.0, "print_time_hours": 4.2, "time_cost_rub": 840.0, "post_processing_cost_rub": 500.0, "subtotal_rub": 3775.0, "quantity": 2, "quantity_discount_percent": 5, "total_rub": 7172.5, "estimated_days": 3 } } ``` **Ошибки:** - 400 — Неподдерживаемый формат файла - 400 — Файл повреждён или не является 3D-моделью - 400 — Модель не является водонепроницаемой (warning, не blocking) - 413 — Файл слишком большой (>50MB) #### 2. GET /api/materials Список доступных материалов с характеристиками. **Response:** ```json [ { "id": 1, "name": "PLA", "category": "basic", "price_per_gram": 25.0, "density_g_cm3": 1.24, "properties": { "max_temp_c": 60, "strength": "medium", "flexibility": "low", "chemical_resistance": "low", "food_safe": true }, "description": "Базовый пластик, подходит для прототипов и декоративных изделий", "color_options": ["white", "black", "gray", "red", "blue", "green", "natural"] } ] ``` #### 3. POST /api/advisor AI-ассистент для выбора материала. Принимает описание задачи, возвращает рекомендацию. **Request:** ```json { "task_description": "Нужен корпус для уличного датчика температуры. Будет стоять на улице, диапазон температур от -30 до +50. Должен быть водонепроницаемым.", "budget_preference": "optimal", "file_info": { "volume_cm3": 42.7, "bounding_box_mm": {"x": 120, "y": 80, "z": 35} } } ``` **Response:** ```json { "recommended_material_id": 4, "recommended_material_name": "PETG", "reasoning": "Для уличного корпуса датчика рекомендую PETG: термостойкость до +80°C, морозостойкость до -40°C, хорошая UV-стойкость и водонепроницаемость. ABS тоже подошёл бы, но PETG проще в печати и не требует закрытой камеры.", "alternatives": [ { "material_id": 2, "name": "ABS", "why": "Выше ударопрочность, но требует постобработки для герметичности" }, { "material_id": 5, "name": "ASA", "why": "Лучшая UV-стойкость, но дороже" } ] } ``` **Реализация:** Отправляем запрос к Claude API (модель claude-sonnet-4-20250514) с системным промптом, содержащим каталог материалов и их свойства. Ключ API хранится в переменной окружения `ANTHROPIC_API_KEY`. #### 4. POST /api/orders Оформление заказа. **Request:** ```json { "calculation_id": "uuid-of-saved-calculation", "client_name": "Иван Петров", "client_phone": "+79001234567", "client_email": "ivan@example.com", "client_company": "ООО Технопарк", "delivery_method": "pickup", "comment": "Нужно к пятнице, нанести резьбу M4 в двух отверстиях" } ``` **Response:** ```json { "order_id": "ORD-2026-0042", "status": "pending", "total_rub": 7172.5, "estimated_ready_date": "2026-04-02" } ``` **Side effect:** Отправляет уведомление в Telegram-бот владельца с деталями заказа. ### Сервис парсинга файлов (file_parser.py) Используемые библиотеки: - `trimesh` — основной парсер. Читает STL (binary + ASCII), OBJ, 3MF, PLY, GLTF. - `numpy-stl` — запасной вариант для STL, если trimesh не справился. Что извлекаем из файла: ```python import trimesh def parse_3d_file(file_path: str, file_extension: str) -> FileInfo: """ Парсит 3D-файл и возвращает геометрические характеристики. Поддерживаемые форматы: .stl, .3mf, .obj STEP-файлы (.step, .stp) — в v2 (требует cadquery/OCP). """ mesh = trimesh.load(file_path, file_type=file_extension) # Если 3MF содержит несколько тел — объединяем if isinstance(mesh, trimesh.Scene): mesh = trimesh.util.concatenate(mesh.dump()) return FileInfo( volume_cm3=mesh.volume / 1000, # mm³ → cm³ surface_area_cm2=mesh.area / 100, # mm² → cm² bounding_box_mm={ "x": mesh.bounding_box.extents[0], "y": mesh.bounding_box.extents[1], "z": mesh.bounding_box.extents[2], }, is_watertight=mesh.is_watertight, triangle_count=len(mesh.faces), ) ``` ### Сервис расчёта цены (price_engine.py) ```python def calculate_price( file_info: FileInfo, material: Material, infill_percent: int = 30, layer_height_mm: float = 0.2, quantity: int = 1, post_processing: list[str] = [], ) -> Calculation: """ Формула: 1. effective_volume = volume_cm3 * (infill_percent / 100) * 0.7 + volume_cm3 * 0.3 (70% объёма масштабируется по infill, 30% — стенки, всегда 100%) 2. weight_g = effective_volume * material.density_g_cm3 3. material_cost = weight_g * material.price_per_gram 4. print_time_h = estimate_print_time(file_info, layer_height_mm, material) 5. time_cost = print_time_h * TIME_RATE_PER_HOUR # ~200 руб/час 6. post_processing_cost = sum стоимостей выбранных операций 7. subtotal = material_cost + time_cost + post_processing_cost 8. total = subtotal * quantity * (1 - volume_discount(quantity)) """ ``` **Оценка времени печати (упрощённая):** ```python def estimate_print_time(file_info, layer_height_mm, material): """ Упрощённая оценка без полного слайсинга. layers = bounding_box_z / layer_height volume_per_layer = volume_cm3 / layers * 1000 # mm³ time_per_layer = volume_per_layer / material.flow_rate_mm3_s / 60 # минуты travel_time_per_layer ≈ 0.3 мин (константа для Bambu Lab) total = layers * (time_per_layer + travel_time) + setup_time """ ``` **Скидки за количество:** - 1 шт — 0% - 2-5 шт — 5% - 6-20 шт — 10% - 21-100 шт — 15% - 101-500 шт — 20% **Стоимость постобработки:** - `sanding` (шлифовка) — 300 руб/шт - `painting` (покраска) — 500 руб/шт - `threading` (нарезка резьбы) — 200 руб/отверстие - `acetone_smoothing` (ацетоновая обработка, только ABS) — 400 руб/шт ### Справочник материалов (seed data) ```python MATERIALS = [ { "name": "PLA", "category": "basic", "density_g_cm3": 1.24, "price_per_gram": 25.0, "flow_rate_mm3_s": 15.0, "max_temp_c": 60, "min_temp_c": -20, "strength": "medium", "flexibility": "low", "chemical_resistance": "low", "uv_resistance": "low", "food_safe": True, "description": "Базовый пластик. Лёгкий в печати, хорошая детализация. Для прототипов и декора.", }, { "name": "PETG", "category": "basic", "density_g_cm3": 1.27, "price_per_gram": 28.0, "flow_rate_mm3_s": 12.0, "max_temp_c": 80, "min_temp_c": -40, "strength": "high", "flexibility": "medium", "chemical_resistance": "medium", "uv_resistance": "medium", "food_safe": True, "description": "Универсальный инженерный пластик. Прочный, химстойкий, подходит для улицы.", }, { "name": "ABS", "category": "basic", "density_g_cm3": 1.04, "price_per_gram": 25.0, "flow_rate_mm3_s": 12.0, "max_temp_c": 100, "min_temp_c": -30, "strength": "high", "flexibility": "low", "chemical_resistance": "medium", "uv_resistance": "low", "food_safe": False, "description": "Термостойкий, ударопрочный. Требует закрытой камеры. Обрабатывается ацетоном.", }, { "name": "PA (Nylon)", "category": "engineering", "density_g_cm3": 1.14, "price_per_gram": 50.0, "flow_rate_mm3_s": 10.0, "max_temp_c": 120, "min_temp_c": -40, "strength": "very_high", "flexibility": "medium", "chemical_resistance": "high", "uv_resistance": "medium", "food_safe": False, "description": "Инженерный пластик. Высокая прочность, износостойкость. Для шестерён, креплений.", }, { "name": "PC (Поликарбонат)", "category": "engineering", "density_g_cm3": 1.20, "price_per_gram": 60.0, "flow_rate_mm3_s": 8.0, "max_temp_c": 140, "min_temp_c": -40, "strength": "very_high", "flexibility": "low", "chemical_resistance": "high", "uv_resistance": "high", "food_safe": False, "description": "Максимальная термостойкость и прочность. Для корпусов, работающих при высоких температурах.", }, { "name": "TPU", "category": "engineering", "density_g_cm3": 1.21, "price_per_gram": 40.0, "flow_rate_mm3_s": 6.0, "max_temp_c": 80, "min_temp_c": -30, "strength": "medium", "flexibility": "very_high", "chemical_resistance": "high", "uv_resistance": "medium", "food_safe": False, "description": "Эластичный пластик, аналог резины. Для прокладок, амортизаторов, гибких деталей.", }, { "name": "PA-CF (Нейлон + углеволокно)", "category": "composite", "density_g_cm3": 1.18, "price_per_gram": 75.0, "flow_rate_mm3_s": 8.0, "max_temp_c": 150, "min_temp_c": -40, "strength": "extreme", "flexibility": "low", "chemical_resistance": "very_high", "uv_resistance": "high", "food_safe": False, "description": "Композит с углеволокном. Максимальная жёсткость и прочность. Замена алюминия.", }, ] ``` ### AI Advisor — системный промпт Файл `app/services/ai_advisor.py` использует Anthropic Python SDK: ```python SYSTEM_PROMPT = """ Ты — эксперт по 3D-печати из инженерных пластиков по технологии FDM. Твоя задача — рекомендовать оптимальный материал для печати на основе описания задачи клиента. Доступные материалы: {materials_json} Правила: 1. Всегда рекомендуй один основной материал и 1-2 альтернативы. 2. Учитывай: температурный режим, механические нагрузки, химическое воздействие, UV, влажность. 3. Если задача не подходит для FDM-печати (слишком мелкие детали, высокая точность) — честно скажи об этом. 4. Отвечай кратко, по делу, на русском языке. 5. Если клиент не указал критичные параметры — задай уточняющие вопросы. Формат ответа — строго JSON: { "recommended_material_id": , "reasoning": "<обоснование на русском>", "alternatives": [{"material_id": , "name": "", "why": "<причина>"}], "questions": ["<вопрос, если нужна доп. информация>"] // пустой массив если вопросов нет } """ ``` ### Telegram-уведомления (telegram_notify.py) При создании заказа отправляем сообщение в Telegram-бот владельца: ```python import httpx async def notify_new_order(order: Order): """Отправляет уведомление о новом заказе в Telegram.""" text = ( f"🆕 Новый заказ #{order.order_id}\n" f"Клиент: {order.client_name}\n" f"Телефон: {order.client_phone}\n" f"Материал: {order.material_name}\n" f"Сумма: {order.total_rub} ₽\n" f"Комментарий: {order.comment or '—'}" ) await httpx.AsyncClient().post( f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage", json={"chat_id": TELEGRAM_CHAT_ID, "text": text} ) ``` Переменные окружения: `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`. --- ## Frontend (Vue 3 + Vite) ### Структура проекта ``` frontend/ ├── src/ │ ├── App.vue │ ├── main.js │ ├── router/ │ │ └── index.js # Vue Router │ ├── stores/ │ │ ├── calculator.js # Pinia store — состояние калькулятора │ │ └── materials.js # Pinia store — материалы │ ├── api/ │ │ └── client.js # Axios instance, базовые запросы │ ├── views/ │ │ ├── CalculatorView.vue # Главная страница с калькулятором │ │ ├── MaterialsView.vue # Каталог материалов │ │ └── OrderView.vue # Форма заказа │ ├── components/ │ │ ├── FileUploader.vue # Drag-and-drop загрузка файла │ │ ├── MaterialPicker.vue # Выбор материала (карточки) │ │ ├── PrintSettings.vue # Настройки печати (infill, layer) │ │ ├── PriceResult.vue # Отображение расчёта │ │ ├── AiAdvisor.vue # Чат с AI-ассистентом │ │ └── OrderForm.vue # Форма заказа │ └── assets/ │ └── styles/ │ └── main.css # Tailwind CSS ├── index.html ├── vite.config.js ├── tailwind.config.js ├── package.json └── Dockerfile ``` ### Зависимости ```json { "dependencies": { "vue": "^3.5", "vue-router": "^4.4", "pinia": "^2.2", "axios": "^1.7" }, "devDependencies": { "vite": "^6.0", "@vitejs/plugin-vue": "^5.1", "tailwindcss": "^3.4", "autoprefixer": "^10.4", "postcss": "^8.4" } } ``` ### Страницы и маршруты | Путь | Компонент | Описание | |------|-----------|----------| | `/` | CalculatorView | Главная: загрузка файла → материал → настройки → цена | | `/materials` | MaterialsView | Каталог материалов с фильтрами | | `/order/:calcId` | OrderView | Форма оформления заказа | ### Компонент FileUploader.vue Требования: - Drag-and-drop зона + кнопка «Выбрать файл» - Принимает: `.stl`, `.3mf`, `.obj` (валидация по расширению на фронте) - Максимальный размер: 50 MB - Отображает имя файла, размер и иконку формата после загрузки - Показывает прогресс-бар при отправке на сервер - При ошибке парсинга — человекочитаемое сообщение ### Компонент MaterialPicker.vue Требования: - Отображает материалы карточками (не dropdown) - Карточка содержит: название, цену за грамм, ключевые свойства (иконки) - Разделение на категории: «Базовые», «Инженерные», «Композитные» - Выбранный материал подсвечивается - Кнопка «Помочь выбрать» открывает AI-ассистент ### Компонент PrintSettings.vue Требования: - Слайдер: заполнение (10%-100%, шаг 10%, default 30%) - Слайдер: высота слоя (0.08-0.4mm, шаг 0.04, default 0.2) - Поле: количество (1-500, default 1) - Чекбоксы: постобработка (шлифовка, покраска, резьба, ацетон) - Подсказки при наведении: как параметр влияет на результат ### Компонент PriceResult.vue Требования: - Показывает разбивку: материал, время, постобработка, скидка, итого - Крупно отображает итоговую цену - Показывает примерный срок изготовления - Кнопка «Оформить заказ» → переход на /order/:calcId - Кнопка «Скачать расчёт (PDF)» — v2, пока не реализуем ### Компонент AiAdvisor.vue Требования: - Модальное окно или выдвижная панель справа - Текстовое поле для описания задачи - Кнопка «Получить рекомендацию» - Отображает рекомендацию: основной материал + альтернативы с обоснованием - Кнопка «Выбрать» рядом с каждой рекомендацией — применяет материал в калькулятор ### Дизайн - Стиль: минималистичный, светлая тема, Tailwind CSS - Акцентный цвет: #2563EB (синий) — кнопки, выделения - Шрифт: Inter (Google Fonts) - Адаптивность: mobile-first, работает на телефоне - Тёмная тема: v2 (не в MVP) --- ## База данных (PostgreSQL) ### Таблицы **materials** ```sql CREATE TABLE materials ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, category VARCHAR(50) NOT NULL, -- basic, engineering, composite density_g_cm3 FLOAT NOT NULL, price_per_gram FLOAT NOT NULL, flow_rate_mm3_s FLOAT NOT NULL, max_temp_c INT, min_temp_c INT, strength VARCHAR(20), -- low, medium, high, very_high, extreme flexibility VARCHAR(20), chemical_resistance VARCHAR(20), uv_resistance VARCHAR(20), food_safe BOOLEAN DEFAULT FALSE, description TEXT, color_options JSONB DEFAULT '[]', is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW() ); ``` **calculations** ```sql CREATE TABLE calculations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), file_name VARCHAR(255) NOT NULL, file_format VARCHAR(10) NOT NULL, file_path VARCHAR(500), volume_cm3 FLOAT NOT NULL, surface_area_cm2 FLOAT, bounding_box JSONB, is_watertight BOOLEAN, triangle_count INT, material_id INT REFERENCES materials(id), infill_percent INT DEFAULT 30, layer_height_mm FLOAT DEFAULT 0.2, quantity INT DEFAULT 1, post_processing JSONB DEFAULT '[]', weight_grams FLOAT, material_cost_rub FLOAT, time_cost_rub FLOAT, post_processing_cost_rub FLOAT, total_rub FLOAT, estimated_days INT, created_at TIMESTAMP DEFAULT NOW() ); ``` **orders** ```sql CREATE TABLE orders ( id SERIAL PRIMARY KEY, order_id VARCHAR(20) UNIQUE NOT NULL, -- ORD-2026-0001 calculation_id UUID REFERENCES calculations(id), client_name VARCHAR(200) NOT NULL, client_phone VARCHAR(20) NOT NULL, client_email VARCHAR(200), client_company VARCHAR(200), delivery_method VARCHAR(50) DEFAULT 'pickup', -- pickup, delivery comment TEXT, status VARCHAR(30) DEFAULT 'pending', -- pending, confirmed, printing, ready, delivered, cancelled total_rub FLOAT NOT NULL, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); ``` --- ## Docker Compose ```yaml version: "3.8" services: db: image: postgres:16-alpine environment: POSTGRES_DB: print3d POSTGRES_USER: print3d POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data ports: - "5432:5432" backend: build: ./backend environment: DATABASE_URL: postgresql+asyncpg://print3d:${DB_PASSWORD}@db:5432/print3d ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN} TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID} UPLOAD_DIR: /app/uploads volumes: - uploads:/app/uploads depends_on: - db ports: - "8000:8000" frontend: build: ./frontend ports: - "5173:5173" nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf - ./nginx/certs:/etc/nginx/certs depends_on: - backend - frontend volumes: pgdata: uploads: ``` --- ## Переменные окружения (.env) ``` DB_PASSWORD= ANTHROPIC_API_KEY= TELEGRAM_BOT_TOKEN= TELEGRAM_CHAT_ID= ``` --- ## Порядок реализации ### Фаза 1 — Backend core (3-4 дня) 1. Инициализация FastAPI проекта, Docker, PostgreSQL 2. Модели SQLAlchemy + Alembic миграции 3. Seed данных по материалам 4. Сервис парсинга файлов (trimesh) 5. Сервис расчёта цены (price engine) 6. Endpoints: POST /api/calculate, GET /api/materials ### Фаза 2 — Frontend core (3-4 дня) 1. Инициализация Vue 3 + Vite + Tailwind 2. FileUploader компонент 3. MaterialPicker компонент 4. PrintSettings + PriceResult компоненты 5. Интеграция с API (Pinia stores + Axios) ### Фаза 3 — AI + Orders (2-3 дня) 1. AI Advisor — интеграция Claude API 2. AiAdvisor компонент (фронт) 3. OrderForm компонент + POST /api/orders 4. Telegram-уведомления ### Фаза 4 — Деплой (1-2 дня) 1. Docker Compose конфигурация 2. Nginx конфиг (reverse proxy + SSL) 3. Деплой на VPS 4. Тестирование end-to-end --- ## Что НЕ входит в MVP (v2) - 3D-превью модели в браузере (Three.js + STLLoader) - Парсинг STEP-файлов (требует cadquery / OpenCASCADE) - Личный кабинет клиента - Онлайн-оплата (ЮKassa / Stripe) - История заказов - Тёмная тема - Скачивание расчёта в PDF - Мультиязычность - SEO-оптимизация (SSR / pre-rendering)