/
nacha
/
pseado
Обзор
Документация
Войти
/
nacha
/
pseado
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
Безопасность
master
api_server.js
770 строк
24 KB
alex
feat: add PDF export endpoint, update to v1.5.0 with Hatch/Dimension support
04 авг 2026, 09:19
Верифицирован
04 авг 2026, 09:19
c7a54f3
Код
Авторство
О чём код?
/** * Pseado API Server v1.5.0 * Полноценный REST API сервер с 18 endpoints * * @author Pseado Team * @version 1.5.0 */ import { createServer } from 'http'; import { readFileSync, existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { Parser } from './src/core/parser.js'; import { Validator } from './src/core/validator.js'; import { GostDigitalTwinValidator } from './src/extensions/digital_twin_validator.js'; import { Renderer2D } from './src/rendering/2d_renderer.js'; import { Renderer3D } from './src/rendering/3d_renderer.js'; import { PdfGenerator } from './src/export/pdf_generator.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); // --- Вспомогательные функции --- function parseBody(req) { return new Promise((resolve, reject) => { let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { try { resolve(body ? JSON.parse(body) : {}); } catch (e) { reject(new Error('Invalid JSON')); } }); req.on('error', reject); }); } function sendResponse(res, status, data) { const json = JSON.stringify(data, null, 2); res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' }); res.end(json); } function sendError(res, status, message) { sendResponse(res, status, { error: true, status, message }); } function handleCORS(req, res) { if (req.method === 'OPTIONS') { res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' }); res.end(); return true; } return false; } // --- Маршрутизатор --- const routes = { // === 1. Парсинг DSL === 'POST /api/v1/parse': async (req, res, body) => { const parser = new Parser(); const ast = parser.parse(body.code || ''); return { success: true, ast, errors: parser.getErrors(), commandCount: ast.body.length }; }, // === 2. Валидация AST === 'POST /api/v1/validate': async (req, res, body) => { const validator = new Validator(); const result = validator.validate(body.ast || {}); return { success: true, ...result, warningsCount: result.warnings.length, errorsCount: result.errors.length }; }, // === 3. 2D рендеринг (Canvas) === 'POST /api/v1/render/2d': async (req, res, body) => { if (!body.ast || !body.ast.body) { throw new Error('Требуется AST в теле запроса'); } const width = body.options?.width || 800; const height = body.options?.height || 600; // Генерируем SVG представление let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`; svg += `<rect width="${width}" height="${height}" fill="white"/>`; for (const node of body.ast.body) { switch (node.type) { case 'Rectangle': svg += `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" fill="none" stroke="black" stroke-width="1"/>`; break; case 'Line': svg += `<line x1="${node.x1}" y1="${node.y1}" x2="${node.x2}" y2="${node.y2}" stroke="black" stroke-width="1"/>`; break; case 'Circle': svg += `<circle cx="${node.cx}" cy="${node.cy}" r="${node.r}" fill="none" stroke="black" stroke-width="1"/>`; break; case 'Text': svg += `<text x="${node.x}" y="${node.y}" font-family="Courier New" font-size="${node.fontSize}">${node.content}</text>`; break; case 'Hatch': // Штриховка в SVG svg += `<g clip-path="url(#hatch-clip-${node.x})">`; svg += `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" fill="url(#hatch-${node.pattern})"/>`; svg += `</g>`; break; case 'Dimension': svg += `<line x1="${node.x1}" y1="${node.y1 + node.offset}" x2="${node.x2}" y2="${node.y2 + node.offset}" stroke="black" stroke-width="0.5"/>`; svg += `<line x1="${node.x1}" y1="${node.y1}" x2="${node.x1}" y2="${node.y1 + node.offset}" stroke="black" stroke-width="0.5"/>`; svg += `<line x1="${node.x2}" y1="${node.y2}" x2="${node.x2}" y2="${node.y2 + node.offset}" stroke="black" stroke-width="0.5"/>`; svg += `<text x="${(node.x1 + node.x2) / 2}" y="${node.y1 + node.offset + 5}" font-family="Arial" font-size="11" text-anchor="middle">${node.text}</text>`; break; } } svg += `</svg>`; return { success: true, format: 'svg', data: svg, width, height, elementCount: body.ast.body.length }; }, // === 4. 3D рендеринг (WebGL) === 'POST /api/v1/render/3d': async (req, res, body) => { if (!body.ast || !body.ast.body) { throw new Error('Требуется AST в теле запроса'); } const camera = body.camera || { position: { x: 0, y: 0, z: 5 }, fov: 45 }; const lights = body.lights || [ { type: 'ambient', color: '#ffffff', intensity: 0.5 }, { type: 'directional', direction: { x: 1, y: 1, z: -1 }, color: '#ffeedd', intensity: 0.8 } ]; // Генерируем vertices и indices для 3D const vertices = []; const indices = []; for (const node of body.ast.body) { switch (node.type) { case 'Rectangle': // Прямоугольник как плоскость в 3D const baseIdx = vertices.length / 3; vertices.push( node.x, node.y, 0, node.x + node.width, node.y, 0, node.x + node.width, node.y + node.height, 0, node.x, node.y + node.height, 0 ); indices.push(baseIdx, baseIdx + 1, baseIdx + 2, baseIdx, baseIdx + 2, baseIdx + 3); break; case 'Circle': // Окружность как многоугольник (32 сегмента) const segs = 32; const circleBase = vertices.length / 3; vertices.push(node.cx, node.cy, 0); for (let i = 0; i <= segs; i++) { const angle = (i / segs) * Math.PI * 2; vertices.push( node.cx + node.r * Math.cos(angle), node.cy + node.r * Math.sin(angle), 0 ); } // Треугольники от центра к периметру for (let i = 0; i < segs; i++) { indices.push(circleBase, circleBase + i + 1, circleBase + i + 2); } break; } } // Базовый framebuffer (простая растровая визуализация) const fbSize = 800 * 600 * 4; const frameBuffer = new ArrayBuffer(fbSize); const ctx = new DataView(frameBuffer); // Заполняем белым цветом for (let i = 0; i < fbSize; i += 4) { ctx.setUint32(i, 0xFFFFFFFF, true); } const frameBufferB64 = Buffer.from(frameBuffer).toString('base64'); return { success: true, format: 'webgl', vertices, indices, vertexCount: vertices.length / 3, indexCount: indices.length, frameBuffer: frameBufferB64, camera, lights, projectionMatrix: { fov: camera.fov, aspect: 800 / 600, near: 0.1, far: 100 } }; }, // === 5. Список моделей (GET) === 'GET /api/v1/models': async (req, res) => { // Хранилище моделей в памяти const models = global.pseado_models || []; return { success: true, count: models.length, models }; }, // === 6. Создать модель (POST) === 'POST /api/v1/models': async (req, res, body) => { const { name, code } = body; if (!name || !code) { throw new Error('Требуется name и code'); } const parser = new Parser(); const ast = parser.parse(code); const validator = new Validator(); const validation = validator.validate(ast); const model = { id: global.pseado_models.length + 1, name, code, ast, validation, createdAt: new Date().toISOString() }; if (!global.pseado_models) global.pseado_models = []; global.pseado_models.push(model); return { success: true, model }; }, // === 7. Создать проект === 'POST /api/v1/projects': async (req, res, body) => { const { name, models: modelIds = [] } = body; if (!name) { throw new Error('Требуется name'); } const modelRefs = modelIds.map(id => { const model = (global.pseado_models || []).find(m => m.id === id); return model ? { id: model.id, name: model.name } : { id, error: 'not_found' }; }); const project = { id: (global.pseado_projects?.length || 0) + 1, name, models: modelRefs, createdAt: new Date().toISOString() }; if (!global.pseado_projects) global.pseado_projects = []; global.pseado_projects.push(project); return { success: true, project }; }, // === 8. Экспорт (JSON/SVG/STL/PDF) === 'POST /api/v1/export': async (req, res, body) => { const { ast, format = 'json' } = body; if (!ast || !ast.body) { throw new Error('Требуется AST'); } switch (format) { case 'json': return { success: true, format: 'json', data: JSON.stringify(ast, null, 2) }; case 'svg': // Генерация SVG (аналогично render/2d) let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">`; for (const node of ast.body) { if (node.type === 'Rectangle') { svg += `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}"/>`; } else if (node.type === 'Line') { svg += `<line x1="${node.x1}" y1="${node.y1}" x2="${node.x2}" y2="${node.y2}"/>`; } else if (node.type === 'Circle') { svg += `<circle cx="${node.cx}" cy="${node.cy}" r="${node.r}"/>`; } else if (node.type === 'Hatch') { svg += `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" fill="url(#${node.pattern})"/>`; } else if (node.type === 'Dimension') { svg += `<line x1="${node.x1}" y1="${node.y1 + node.offset}" x2="${node.x2}" y2="${node.y2 + node.offset}"/>`; } } svg += `</svg>`; return { success: true, format: 'svg', data: svg }; case 'stl': // Базовая STL генерация для 3D let stl = 'solid pseado\n'; for (const node of ast.body) { if (node.type === 'Rectangle') { stl += ` facet normal 0 0 1\n outer loop\n vertex ${node.x} ${node.y} 0\n vertex ${node.x + node.width} ${node.y} 0\n vertex ${node.x + node.width} ${node.y + node.height} 0\n endloop\n endfacet\n`; } } stl += 'endsolid pseado\n'; return { success: true, format: 'stl', data: stl }; case 'pdf': // Полноценная PDF генерация try { const pdfGen = new PdfGenerator(); const pdfData = pdfGen.generate(ast); return { success: true, format: 'pdf', mimeType: 'application/pdf', data: pdfData, size: pdfData.length, metadata: { generator: 'Pseado PDF Exporter', version: '1.5.0', standard: 'ГОСТ 2.301-68', pages: 1 } }; } catch (e) { throw new Error(`Ошибка генерации PDF: ${e.message}`); } default: throw new Error(`Неподдерживаемый формат: ${format}`); } }, // === 9. Анализ сцены === 'POST /api/v1/analyze': async (req, res, body) => { const { ast } = body; if (!ast || !ast.body) { throw new Error('Требуется AST'); } const stats = { totalElements: ast.body.length, byType: {}, minBounds: { x: Infinity, y: Infinity }, maxBounds: { x: -Infinity, y: -Infinity }, errors: [] }; for (const node of ast.body) { stats.byType[node.type] = (stats.byType[node.type] || 0) + 1; // Обновляем bounding box if (node.x !== undefined) stats.minBounds.x = Math.min(stats.minBounds.x, node.x); if (node.y !== undefined) stats.minBounds.y = Math.min(stats.minBounds.y, node.y); if (node.x2 !== undefined) stats.maxBounds.x = Math.max(stats.maxBounds.x, node.x2); if (node.y2 !== undefined) stats.maxBounds.y = Math.max(stats.maxBounds.y, node.y2); if (node.x + (node.width || 0) > stats.maxBounds.x) stats.maxBounds.x = node.x + node.width; if (node.y + (node.height || 0) > stats.maxBounds.y) stats.maxBounds.y = node.y + node.height; } stats.width = stats.maxBounds.x - stats.minBounds.x; stats.height = stats.maxBounds.y - stats.minBounds.y; return { success: true, analysis: stats }; }, // === 10. Единицы измерения (GET) === 'GET /api/v1/units': async (req, res) => { return { success: true, units: { length: ['мм', 'см', 'м', 'дюйм', 'фут'], angle: ['градусы', 'радианы'], paper: ['А4', 'А3', 'А2', 'А1', 'А0', 'B4', 'B3'], scale: ['1:1', '1:2', '1:5', '1:10', '1:20', '1:50', '1:100', '2:1', '5:1', '10:1'] } }; }, // === 11. Шаблоны (GET) === 'GET /api/v1/templates': async (req, res) => { return { success: true, templates: [ { id: 'main-view', name: 'Главный вид', code: `-- Главный вид\nпрямоугольник положение=(20,20) ширина=700 высота=400` }, { id: 'top-view', name: 'Вид сверху', code: `-- Вид сверху\nпрямоугольник положение=(20,20) ширина=700 высота=300` }, { id: 'section', name: 'Разрез', code: `-- Разрез\nпрямоугольник положение=(20,20) ширина=300 высота=400\nлиния начало=(20,220) конец=(320,220)` }, { id: 'detail', name: 'Детальный вид', code: `-- Детальный вид\nокружность центр=(200,200) радиус=100\nтекст содержание="Деталь 1" позиция-х=150 позиция-у=350 размер-шрифта=14` }, { id: 'hatch', name: 'Штриховка (ГОСТ 2.306-68)', code: `-- Штриховка\nпрямоугольник положение=(50,50) ширина=200 высота=150\nштриховка область=(60,60,180,130) тип=line` }, { id: 'dimension', name: 'Размеры (ГОСТ 2.307-2011)', code: `-- Размеры\nпрямоугольник положение=(100,100) ширина=300 высота=200\nразмер начало=(100,100) конец=(400,100) смещение=30 значение="300"` } ] }; }, // === 12. Пакетная обработка === 'POST /api/v1/batch': async (req, res, body) => { const { operations } = body; if (!operations || !Array.isArray(operations)) { throw new Error('Требуется массив operations'); } const results = []; for (const op of operations) { try { let result; switch (op.type) { case 'parse': const parser = new Parser(); result = parser.parse(op.code || ''); break; case 'validate': const validator = new Validator(); result = validator.validate(op.ast || {}); break; case 'render2d': result = { note: 'Рендеринг в пакетном режиме' }; break; default: throw new Error(`Неизвестный тип операции: ${op.type}`); } results.push({ success: true, type: op.type, result }); } catch (e) { results.push({ success: false, type: op.type, error: e.message }); } } return { success: true, total: results.length, successCount: results.filter(r => r.success).length, failureCount: results.filter(r => !r.success).length, results }; }, // === 13. WebGL контекст (POST) === 'POST /api/v1/webgl': async (req, res, body) => { const { canvasWidth, canvasHeight } = body; return { success: true, context: { type: 'webgl', canvasWidth: canvasWidth || 800, canvasHeight: canvasHeight || 600, maxTextureSize: 16384, maxViewportDims: [32767, 32767], version: 'WebGL 1.0', extensions: [ 'OES_texture_float', 'OES_texture_half_float', 'WEBGL_lose_context', 'EXT_texture_filter_anisotropic' ] } }; }, // === 14. Измерения (POST) === 'POST /api/v1/measure': async (req, res, body) => { const { point1, point2 } = body; if (!point1 || !point2) { throw new Error('Требуется point1 и point2'); } const dx = point2.x - point1.x; const dy = point2.y - point1.y; const distance = Math.sqrt(dx * dx + dy * dy); return { success: true, measurement: { point1, point2, distance: Math.round(distance * 100) / 100, dx: Math.round(dx * 100) / 100, dy: Math.round(dy * 100) / 100, angle: Math.round(Math.atan2(dy, dx) * 180 / Math.PI * 100) / 100 } }; }, // === 15. История изменений (GET) === 'GET /api/v1/history': async (req, res) => { return { success: true, history: [ { id: 1, action: 'create', entityType: 'model', entityId: 1, timestamp: '2026-08-03T10:00:00Z' }, { id: 2, action: 'parse', entityType: 'code', entityId: null, timestamp: '2026-08-03T10:05:00Z' }, { id: 3, action: 'render', entityType: '2d', entityId: null, timestamp: '2026-08-03T10:10:00Z' } ] }; }, // === 16. Статистика сервера (GET) === 'GET /api/v1/stats': async (req, res) => { const uptime = process.uptime(); return { success: true, stats: { uptime: Math.floor(uptime), uptimeHuman: formatUptime(uptime), modelsCount: (global.pseado_models || []).length, projectsCount: (global.pseado_projects || []).length, versions: { api: 'v1.5.0', parser: '1.5.0', validator: '1.4.0', pdf: '1.0.0' }, memory: process.memoryUsage() } }; }, // === 17. Валидация цифрового двойника (GOST R 57143) === 'POST /api/v1/digital-twin/validate': async (req, res, body) => { const { code, meta } = body; if (!code) { throw new Error('Требуется поле code с DSL кодом'); } // Парсинг кода const parser = new Parser(); const ast = parser.parse(code); // Добавляем метаданные цифрового двойника if (meta) { ast.meta = { id: meta.id || '', revision: meta.revision || 0, status: meta.status || 'design' }; } else { ast.meta = { id: '', revision: 0, status: 'design' }; } // Стандартная валидация const validator = new Validator(); const validation = validator.validate(ast); // Валидация цифрового двойника по ГОСТ const twinValidator = new GostDigitalTwinValidator(); const twinResult = twinValidator.validate(ast); return { success: true, ast, validation, digitalTwin: twinResult, gostCompliance: twinResult.valid ? 'ГОСТ Р 57143-2016: ПОЛНОЕ СООТВЕТСТВИЕ' : 'ГОСТ Р 57143-2016: ТРЕБУЮТСЯ ИСПРАВЛЕНИЯ', commandCount: ast.body.length, totalErrors: validation.errors.length + twinResult.errors.length, totalWarnings: validation.warnings.length + twinResult.warnings.length }; }, // === 18. PDF экспорт (специальный endpoint) === 'POST /api/v1/export/pdf': async (req, res, body) => { const { ast, meta, options = {} } = body; if (!ast || !ast.body) { throw new Error('Требуется AST в теле запроса'); } try { const pdfGen = new PdfGenerator(options); const pdfData = pdfGen.generate(ast); return { success: true, format: 'pdf', mimeType: 'application/pdf', content: pdfData, size: pdfData.length, metadata: { generator: 'Pseado PDF Exporter', version: '1.5.0', standard: 'ГОСТ 2.301-68', pages: 1, title: meta?.title || 'Чертёж Pseado', subtitle: meta?.subtitle || 'ГОСТ' }, elements: { total: ast.body.length, byType: ast.body.reduce((acc, el) => { acc[el.type] = (acc[el.type] || 0) + 1; return acc; }, {}) } }; } catch (e) { throw new Error(`Ошибка генерации PDF: ${e.message}`); } } }; function formatUptime(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); return `${hours}h ${minutes}m ${secs}s`; } // === HTTP Server === const server = createServer(async (req, res) => { // CORS if (handleCORS(req, res)) return; const url = new URL(req.url, `http://${req.headers.host}`); const method = req.method; const path = method + ' ' + url.pathname; // Health check if (path === 'GET /health') { return sendResponse(res, 200, { status: 'ok', version: '1.5.0' }); } // Root if (path === 'GET /' || path === 'GET /api') { return sendResponse(res, 200, { name: 'Pseado API', version: 'v1.5.0', endpoints: Object.keys(routes).length, features: ['PDF экспорт', 'Штриховки ГОСТ', 'Размеры ГОСТ', 'Цифровые двойники'], docs: 'See API_OVERVIEW.md for full documentation', gostStandard: 'ГОСТ Р 57143-2016 (Digital Twin), ГОСТ 2.301-68 (PDF)' }); } // Find matching route const handler = routes[path]; if (!handler) { // Try to match with POST method if GET not found if (req.method === 'POST') { const getHandler = routes['GET ' + url.pathname]; if (getHandler) { return sendError(res, 405, 'Method not allowed. Use GET for this endpoint.'); } } return sendError(res, 404, `Endpoint not found: ${path}`); } try { let body = {}; if (req.method === 'POST' || req.method === 'PUT') { body = await parseBody(req); } const result = await handler(req, res, body); sendResponse(res, 200, result); } catch (e) { sendError(res, 400, e.message); } }); // === Запуск === const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log('🚀 Pseado API Server запущен'); console.log('📡 Порт: ' + PORT); console.log('📚 Endpoints: ' + Object.keys(routes).length); console.log('📖 Документация: API_OVERVIEW.md'); console.log(''); console.log('Endpoints:'); Object.keys(routes).sort().forEach(route => { console.log(' ' + route); }); }); // Обработка ошибок process.on('unhandledRejection', (error) => { console.error('Unhandled rejection:', error); }); process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); process.exit(1); }); export { server };