29 KiB
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
- frontend — Node 20 + Vite dev server (в проде — собранная статика через Nginx)
- backend — Python 3.12 + FastAPI + Uvicorn
- db — PostgreSQL 16
- 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
{
"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:
[
{
"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:
{
"task_description": "Нужен корпус для уличного датчика температуры. Будет стоять на улице, диапазон температур от -30 до +50. Должен быть водонепроницаемым.",
"budget_preference": "optimal",
"file_info": {
"volume_cm3": 42.7,
"bounding_box_mm": {"x": 120, "y": 80, "z": 35}
}
}
Response:
{
"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:
{
"calculation_id": "uuid-of-saved-calculation",
"client_name": "Иван Петров",
"client_phone": "+79001234567",
"client_email": "ivan@example.com",
"client_company": "ООО Технопарк",
"delivery_method": "pickup",
"comment": "Нужно к пятнице, нанести резьбу M4 в двух отверстиях"
}
Response:
{
"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 не справился.
Что извлекаем из файла:
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)
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))
"""
Оценка времени печати (упрощённая):
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)
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:
SYSTEM_PROMPT = """
Ты — эксперт по 3D-печати из инженерных пластиков по технологии FDM.
Твоя задача — рекомендовать оптимальный материал для печати на основе описания задачи клиента.
Доступные материалы:
{materials_json}
Правила:
1. Всегда рекомендуй один основной материал и 1-2 альтернативы.
2. Учитывай: температурный режим, механические нагрузки, химическое воздействие, UV, влажность.
3. Если задача не подходит для FDM-печати (слишком мелкие детали, высокая точность) — честно скажи об этом.
4. Отвечай кратко, по делу, на русском языке.
5. Если клиент не указал критичные параметры — задай уточняющие вопросы.
Формат ответа — строго JSON:
{
"recommended_material_id": <int>,
"reasoning": "<обоснование на русском>",
"alternatives": [{"material_id": <int>, "name": "<str>", "why": "<причина>"}],
"questions": ["<вопрос, если нужна доп. информация>"] // пустой массив если вопросов нет
}
"""
Telegram-уведомления (telegram_notify.py)
При создании заказа отправляем сообщение в Telegram-бот владельца:
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
Зависимости
{
"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
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
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
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
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=<strong_password>
ANTHROPIC_API_KEY=<claude_api_key>
TELEGRAM_BOT_TOKEN=<telegram_bot_token>
TELEGRAM_CHAT_ID=<your_telegram_chat_id>
Порядок реализации
Фаза 1 — Backend core (3-4 дня)
- Инициализация FastAPI проекта, Docker, PostgreSQL
- Модели SQLAlchemy + Alembic миграции
- Seed данных по материалам
- Сервис парсинга файлов (trimesh)
- Сервис расчёта цены (price engine)
- Endpoints: POST /api/calculate, GET /api/materials
Фаза 2 — Frontend core (3-4 дня)
- Инициализация Vue 3 + Vite + Tailwind
- FileUploader компонент
- MaterialPicker компонент
- PrintSettings + PriceResult компоненты
- Интеграция с API (Pinia stores + Axios)
Фаза 3 — AI + Orders (2-3 дня)
- AI Advisor — интеграция Claude API
- AiAdvisor компонент (фронт)
- OrderForm компонент + POST /api/orders
- Telegram-уведомления
Фаза 4 — Деплой (1-2 дня)
- Docker Compose конфигурация
- Nginx конфиг (reverse proxy + SSL)
- Деплой на VPS
- Тестирование end-to-end
Что НЕ входит в MVP (v2)
- 3D-превью модели в браузере (Three.js + STLLoader)
- Парсинг STEP-файлов (требует cadquery / OpenCASCADE)
- Личный кабинет клиента
- Онлайн-оплата (ЮKassa / Stripe)
- История заказов
- Тёмная тема
- Скачивание расчёта в PDF
- Мультиязычность
- SEO-оптимизация (SSR / pre-rendering)