1
This commit is contained in:
@@ -41,6 +41,7 @@ export const useCanvas = () => {
|
||||
}
|
||||
|
||||
const parseCanvasJson = (json: any): CanvasData => {
|
||||
if (!json) return { nodes: [], edges: [] }
|
||||
const nodes = (json.nodes || []).map((n: any) => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
@@ -88,11 +89,14 @@ export const useCanvas = () => {
|
||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
const loadCanvas = async (id: string): Promise<CanvasData> => {
|
||||
const data = await $fetch(`/api/canvas/${id}`, {
|
||||
const loadCanvas = async (id: string): Promise<{ canvas: CanvasData, role: string }> => {
|
||||
const data = await $fetch<{ canvas: any, role: string }>(`/api/canvas/${id}`, {
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
return parseCanvasJson(data)
|
||||
return {
|
||||
canvas: parseCanvasJson(data.canvas),
|
||||
role: data.role
|
||||
}
|
||||
}
|
||||
|
||||
const updateNodeState = async (nodeId: string, state: string) => {
|
||||
|
||||
@@ -8,16 +8,19 @@ export default defineEventHandler(async (event) => {
|
||||
const canvas = await prisma.canvas.findUnique({ where: { id } })
|
||||
if (!canvas) throw createError({ statusCode: 404, message: 'Canvas not found' })
|
||||
|
||||
const user = tryLazyEventHandler(event, () => requireAuth(event)) || { id: null, role: 'guest' }
|
||||
let user = { id: null, role: 'guest' } as any
|
||||
try {
|
||||
user = requireAuth(event)
|
||||
} catch (e) {}
|
||||
|
||||
// Определение роли
|
||||
let userRole = 'viewer'
|
||||
if (user.id === canvas.ownerId) userRole = 'owner'
|
||||
else if (user.role === 'admin') userRole = 'admin'
|
||||
else if (canvas.groupId) {
|
||||
const membership = await prisma.groupMember.findUnique({
|
||||
const membership = user.id ? await prisma.groupMember.findUnique({
|
||||
where: { userId_groupId: { userId: user.id as string, groupId: canvas.groupId } }
|
||||
})
|
||||
}) : null
|
||||
if (membership) userRole = membership.role
|
||||
else if (!canvas.isPublic) throw createError({ statusCode: 403, message: 'Access denied' })
|
||||
} else if (!canvas.isPublic) {
|
||||
|
||||
@@ -29,10 +29,12 @@ export default defineEventHandler(async (event) => {
|
||||
let completedTasks = 0
|
||||
|
||||
for (const canvas of canvases) {
|
||||
const nodes = (canvas.nodes as any[]) || []
|
||||
const tasks = nodes.filter(n => n.type === 'task')
|
||||
totalTasks += tasks.length
|
||||
completedTasks += tasks.filter(t => t.data?.state === 'completed' || t.state === 'completed').length
|
||||
const nodes = canvas.nodes
|
||||
if (Array.isArray(nodes)) {
|
||||
const tasks = nodes.filter(n => n.type === 'task')
|
||||
totalTasks += tasks.length
|
||||
completedTasks += tasks.filter(t => (t.data?.state === 'completed' || t.state === 'completed')).length
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Ачивки (пока возвращаем 0, так как модели нет в схеме)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"nodes":[
|
||||
{"id":"root-1","type":"text","text":"# 📸 Процесс создания фото-коллажа из людей\n\nЭтот проект описывает полный цикл: от организации фотосъемки до программной реализации динамического коллажа.","x":0,"y":0,"width":400,"height":180,"color":"4"},
|
||||
|
||||
{"id":"photo-title","type":"text","text":"# 📷 Этап 1: Фотографирование","x":-450,"y":-300,"width":350,"height":80,"color":"1"},
|
||||
{"id":"photo-1","type":"text","text":"## 🛠️ Подготовка и оборудование\n\n- **Камера**: Full-frame или качественный смартфон с поддержкой RAW.\n- **Освещение**: \n - Минимум 3 точки света (рисующий, заполняющий, контровой).\n - Использование софтбоксов для мягких теней.\n- **Фон**: Однотонный (серый или белый) для легкой обрезки, либо хромакей (зеленый).","x":-450,"y":-200,"width":350,"height":250,"color":"1"},
|
||||
{"id":"photo-2","type":"text","text":"## 🎭 Съемка и позирование\n\n- **Единообразие**: Съемка всех людей с одного ракурса (на уровне глаз) и при одинаковом освещении.\n- **Вариативность**: Запрос нескольких эмоций (нейтральная, улыбка, удивление) для динамики в коллаже.\n- **Техническая чистота**: Глубина резкости (f/8 - f/11), чтобы все лицо было в фокусе.","x":-450,"y":80,"width":350,"height":220,"color":"1"},
|
||||
{"id":"photo-3","type":"text","text":"## 📁 Организация данных\n\n- **Формат**: Сохранение в PNG с альфа-каналом (после обработки).\n- **Метаданные**: Именование файлов по ID пользователя (`user_123_pose_1.png`).\n- **Бэкап**: Облачное хранилище (S3/Drive) для исходников.","x":-450,"y":330,"width":350,"height":180,"color":"1"},
|
||||
|
||||
{"id":"collage-title","type":"text","text":"# 🧩 Этап 2: Сборка коллажа","x":0,"y":250,"width":400,"height":80,"color":"2"},
|
||||
{"id":"collage-1","type":"text","text":"## ✂️ Обработка и AI\n\n- **Background Removal**: Автоматическая обрезка фона с помощью AI (например, модель Segment Anything от Meta или API Remove.bg).\n- **Face Alignment**: Центрирование лиц по глазам для гармоничного вида в сетке.\n- **Цветокоррекция**: Применение единого пресета (LUT) ко всем фото для визуальной целостности.","x":0,"y":350,"width":400,"height":220,"color":"2"},
|
||||
{"id":"collage-2","type":"text","text":"## 📐 Алгоритмы компоновки\n\n- **Grid Layout**: Классическая сетка (просто, но скучно).\n- **Masonry/Packing**: Плотная упаковка фото разного размера (алгоритм Bin Packing).\n- **Artistic Mosaic**: Генерация большой картинки из маленьких лиц (фото-мозаика).","x":0,"y":600,"width":400,"height":200,"color":"2"},
|
||||
|
||||
{"id":"prog-title","type":"text","text":"# 💻 Этап 3: Программная часть","x":450,"y":-300,"width":400,"height":80,"color":"3"},
|
||||
{"id":"prog-1","type":"text","text":"## ⚙️ Backend (Node.js/Nuxt)\n\n- **Загрузка**: API на базе `h3` с использованием `multer` или `formidable`.\n- **Обработка**: Библиотека **Sharp** для изменения размера, сжатия и наложения водяных знаков.\n- **Очереди**: Использование `BullMQ` + `Redis` для тяжелых операций генерации коллажа.","x":450,"y":-200,"width":400,"height":200,"color":"3"},
|
||||
{"id":"prog-2","type":"text","text":"## 🎨 Frontend (Vue 3 / Canvas)\n\n- **Рендеринг**: \n - HTML5 Canvas API для статических коллажей.\n - `CSS Grid` + `Framer Motion` для интерактивных веб-коллажей.\n- **Оптимизация**: Lazy loading изображений, использование WebP формата.","x":450,"y":30,"width":400,"height":180,"color":"3"},
|
||||
{"id":"prog-3","type":"text","text":"## 🗄️ Хранение и БД\n\n- **Prisma + PostgreSQL**:\n - Таблица `Photos`: `id`, `userId`, `url`, `coordinates` (в коллаже).\n- **S3 Storage**: Хранение самих файлов изображений (MinIO для локальных тестов).","x":450,"y":240,"width":400,"height":180,"color":"3"},
|
||||
{"id":"prog-4","type":"text","text":"## 🤖 AI Интеграция\n\n- **Face Detection**: Использование `face-api.js` на фронтенде или `Mediapipe` на бэкенде для автоматического определения границ лица.\n- **Smart Crop**: Обрезка фото так, чтобы лицо всегда было в центре кадра.","x":450,"y":450,"width":400,"height":200,"color":"3"}
|
||||
],
|
||||
"edges":[
|
||||
{"id":"e1","fromNode":"root-1","fromSide":"left","toNode":"photo-title","toSide":"right","color":"1"},
|
||||
{"id":"e2","fromNode":"root-1","fromSide":"bottom","toNode":"collage-title","toSide":"top","color":"2"},
|
||||
{"id":"e3","fromNode":"root-1","fromSide":"right","toNode":"prog-title","toSide":"left","color":"3"},
|
||||
|
||||
{"id":"ep1","fromNode":"photo-title","fromSide":"bottom","toNode":"photo-1","toSide":"top","color":"1"},
|
||||
{"id":"ep2","fromNode":"photo-1","fromSide":"bottom","toNode":"photo-2","toSide":"top","color":"1"},
|
||||
{"id":"ep3","fromNode":"photo-2","fromSide":"bottom","toNode":"photo-3","toSide":"top","color":"1"},
|
||||
|
||||
{"id":"ec1","fromNode":"collage-title","fromSide":"bottom","toNode":"collage-1","toSide":"top","color":"2"},
|
||||
{"id":"ec2","fromNode":"collage-1","fromSide":"bottom","toNode":"collage-2","toSide":"top","color":"2"},
|
||||
|
||||
{"id":"eg1","fromNode":"prog-title","fromSide":"bottom","toNode":"prog-1","toSide":"top","color":"3"},
|
||||
{"id":"eg2","fromNode":"prog-1","fromSide":"bottom","toNode":"prog-2","toSide":"top","color":"3"},
|
||||
{"id":"eg3","fromNode":"prog-2","fromSide":"bottom","toNode":"prog-3","toSide":"top","color":"3"},
|
||||
{"id":"eg4","fromNode":"prog-3","fromSide":"bottom","toNode":"prog-4","toSide":"top","color":"3"},
|
||||
|
||||
{"id":"e-photo-to-collage","fromNode":"photo-3","fromSide":"right","toNode":"collage-1","toSide":"left","color":"1","label":"Передача файлов"},
|
||||
{"id":"e-prog-to-collage","fromNode":"prog-4","fromSide":"left","toNode":"collage-1","toSide":"right","color":"3","label":"Автоматизация"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"nodes":[
|
||||
{"id":"root","type":"text","text":"# 🏎️ Пинбол: Дистанционное управление (Sensors-to-Game)\n\nКонцепция игры, где смартфон выступает в роли физического контроллера, передавая наклон и ускорение на основной игровой экран через браузер.","x":0,"y":0,"width":450,"height":180,"color":"4"},
|
||||
|
||||
{"id":"controller-title","type":"text","text":"# 📱 Смартфон-контроллер (Frontend)","x":-550,"y":-350,"width":400,"height":80,"color":"1"},
|
||||
{"id":"ctrl-1","type":"text","text":"## 📡 Получение данных сенсоров\n\n- **DeviceOrientationEvent**: Получение углов наклона (alpha, beta, gamma).\n- **DeviceMotionEvent**: Регистрация резких встряхиваний (акселерометр) для «ударов» по столу.\n- **Permissions API**: Обработка запроса разрешений (обязательно для iOS 13+).\n- **Фильтрация**: Использование фильтра низких частот (Low-pass filter) для сглаживания «шума» сенсоров.","x":-550,"y":-250,"width":400,"height":250,"color":"1"},
|
||||
{"id":"ctrl-2","type":"text","text":"## 🎨 Интерфейс управления\n\n- **Визуальный фидбек**: Отображение 3D-модели смартфона, повторяющей наклоны.\n- **Haptic Feedback**: Вибрация при столкновении мяча с флиппером.\n- **Калибровка**: Кнопка «Центрировать», чтобы установить текущее положение как нулевое.","x":-550,"y":30,"width":400,"height":180,"color":"1"},
|
||||
{"id":"ctrl-3","type":"text","text":"## 🔋 Оптимизация\n\n- **Wake Lock API**: Предотвращение выключения экрана во время игры.\n- **Throttling**: Ограничение частоты отправки данных (например, 60 раз в секунду).","x":-550,"y":240,"width":400,"height":140,"color":"1"},
|
||||
|
||||
{"id":"game-title","type":"text","text":"# 🖥️ Игровой Движок (Display)","x":550,"y":-350,"width":450,"height":80,"color":"2"},
|
||||
{"id":"game-1","type":"text","text":"## 🏗️ Физика (Matter.js / P2.js)\n\n- **Гравитация**: Динамическое изменение вектора гравитации в зависимости от наклона телефона игрока.\n- **Коллизии**: Группы столкновений для мячей, стенок и флипперов.\n- **Friction/Restitution**: Настройка упругости для эффекта «пинбола».","x":550,"y":-250,"width":450,"height":200,"color":"2"},
|
||||
{"id":"game-2","type":"text","text":"## 🎮 Игровые Механики\n\n- **PvP Режим**: Поле разделено на две части. Цель — загнать мяч в ворота противника.\n- **Power-ups**: Появление бонусов (увеличение мяча, замедление времени, магнит).\n- **Multi-ball**: Возможность управления несколькими мячами одновременно.","x":550,"y":-20,"width":450,"height":200,"color":"2"},
|
||||
{"id":"game-3","type":"text","text":"## 📺 Визуализация\n\n- **Three.js / PixiJS**: 3D или высокопроизводительный 2D рендеринг.\n- **Particle System**: Эффекты искр и взрывов при столкновениях.","x":550,"y":210,"width":450,"height":150,"color":"2"},
|
||||
|
||||
{"id":"comm-title","type":"text","text":"# ⚡ Связь и Синхронизация","x":0,"y":250,"width":450,"height":80,"color":"3"},
|
||||
{"id":"comm-1","type":"text","text":"## 🔗 Транспорт данных\n\n- **WebSockets (Socket.io)**: Минимальная задержка (Latency < 20ms).\n- **WebRTC (DataChannels)**: P2P связь между телефоном и экраном для еще большей скорости.\n- **Firebase Realtime DB**: Как альтернатива для быстрого прототипирования.","x":-230,"y":350,"width":350,"height":200,"color":"3"},
|
||||
{"id":"comm-2","type":"text","text":"## 🛠️ Архитектура сессии\n\n- **QR-код**: Автоматическая генерация ссылки на контроллер для конкретной комнаты.\n- **State Sync**: Игровой экран — «Источник Истины» (Authoritative Server).\n- **Interpolation**: Сглаживание движения на экране при потере пакетов от телефона.","x":150,"y":350,"width":350,"height":200,"color":"3"},
|
||||
|
||||
{"id":"future-title","type":"text","text":"# 🚀 Масштабирование идеи","x":0,"y":600,"width":450,"height":80,"color":"6"},
|
||||
{"id":"future-1","type":"text","text":"## 🌍 Глобальные возможности\n\n- **Турниры**: Глобальная таблица лидеров (Leaderboard).\n- **Кастомизация**: Скины для столов и мячей (NFT/Collectibles).\n- **AR Mode**: Проекция пинбольного стола на реальный пол через камеру телефона.","x":0,"y":700,"width":450,"height":180,"color":"6"}
|
||||
],
|
||||
"edges":[
|
||||
{"id":"e1","fromNode":"root","fromSide":"left","toNode":"controller-title","toSide":"right","color":"1"},
|
||||
{"id":"e2","fromNode":"root","fromSide":"right","toNode":"game-title","toSide":"left","color":"2"},
|
||||
{"id":"e3","fromNode":"root","fromSide":"bottom","toNode":"comm-title","toSide":"top","color":"3"},
|
||||
{"id":"e4","fromNode":"comm-title","fromSide":"bottom","toNode":"future-title","toSide":"top","color":"6"},
|
||||
|
||||
{"id":"ec1","fromNode":"controller-title","fromSide":"bottom","toNode":"ctrl-1","toSide":"top","color":"1"},
|
||||
{"id":"ec2","fromNode":"ctrl-1","fromSide":"bottom","toNode":"ctrl-2","toSide":"top","color":"1"},
|
||||
{"id":"ec3","fromNode":"ctrl-2","fromSide":"bottom","toNode":"ctrl-3","toSide":"top","color":"1"},
|
||||
|
||||
{"id":"eg1","fromNode":"game-title","fromSide":"bottom","toNode":"game-1","toSide":"top","color":"2"},
|
||||
{"id":"eg2","fromNode":"game-1","fromSide":"bottom","toNode":"game-2","toSide":"top","color":"2"},
|
||||
{"id":"eg3","fromNode":"game-2","fromSide":"bottom","toNode":"game-3","toSide":"top","color":"2"},
|
||||
|
||||
{"id":"em1","fromNode":"comm-title","fromSide":"bottom","toNode":"comm-1","toSide":"top","color":"3"},
|
||||
{"id":"em2","fromNode":"comm-title","fromSide":"bottom","toNode":"comm-2","toSide":"top","color":"3"},
|
||||
|
||||
{"id":"flow-1","fromNode":"ctrl-1","fromSide":"right","toNode":"comm-1","toSide":"left","color":"1","label":"Сенсоры -> Сокеты"},
|
||||
{"id":"flow-2","fromNode":"comm-2","fromSide":"right","toNode":"game-1","toSide":"left","color":"2","label":"Сокеты -> Гравитация"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
async function main() {
|
||||
const prisma = new PrismaClient()
|
||||
try {
|
||||
const count = await prisma.user.count()
|
||||
console.log('User count:', count)
|
||||
const canvases = await prisma.canvas.findMany({ take: 1 })
|
||||
console.log('Sample canvas:', canvases[0]?.id)
|
||||
} catch (e) {
|
||||
console.error('Prisma connection error:', e)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user