Files
3Dviewer/api/src/routes/models.routes.ts
likegears 7af9c323f6 Initial commit: 3D Viewer application
Features:
- Vue 3 frontend with Three.js/Online3DViewer
- Node.js API with PostgreSQL and Redis
- Python worker for model conversion
- Docker Compose for deployment
- ViewCube navigation with drag rotation and 90° snap
- Cross-section, exploded view, and render settings
- Parts tree with visibility controls

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 14:00:17 +08:00

304 lines
6.8 KiB
TypeScript

import { Router, type IRouter } from 'express';
import multer from 'multer';
import { validate, schemas } from '../middleware/validation.middleware.js';
import { NotFoundError } from '../middleware/error.middleware.js';
import * as modelsService from '../services/models.service.js';
import { addThumbnailJob } from '../services/queue.service.js';
import logger from '../utils/logger.js';
const router: IRouter = Router();
// Configure multer for thumbnail uploads (memory storage for small images)
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 2 * 1024 * 1024, // 2MB max
},
fileFilter: (req, file, cb) => {
if (file.mimetype === 'image/png' || file.mimetype === 'image/jpeg') {
cb(null, true);
} else {
cb(new Error('Only PNG and JPEG images are allowed'));
}
},
});
/**
* GET /models - List all models
*/
router.get('/', validate(schemas.modelListQuery, 'query'), async (req, res, next) => {
try {
const query = req.query as unknown as {
search?: string;
status?: 'pending' | 'processing' | 'completed' | 'failed';
format?: string;
limit: number;
offset: number;
};
const result = await modelsService.getModels({
search: query.search,
status: query.status,
format: query.format,
limit: Number(query.limit) || 20,
offset: Number(query.offset) || 0,
});
res.json({
success: true,
data: result.models,
meta: {
total: result.total,
limit: Number(query.limit) || 20,
offset: Number(query.offset) || 0,
},
});
} catch (error) {
next(error);
}
});
/**
* GET /models/:id - Get a single model
*/
router.get('/:id', validate(schemas.uuidParam, 'params'), async (req, res, next) => {
try {
const { id } = req.params;
const model = await modelsService.getModelById(id);
if (!model) {
throw new NotFoundError('Model not found');
}
res.json({
success: true,
data: model,
});
} catch (error) {
next(error);
}
});
/**
* GET /models/:id/parts - Get model parts
*/
router.get('/:id/parts', validate(schemas.uuidParam, 'params'), async (req, res, next) => {
try {
const { id } = req.params;
const model = await modelsService.getModelById(id);
if (!model) {
throw new NotFoundError('Model not found');
}
const parts = await modelsService.getModelParts(id);
res.json({
success: true,
data: parts,
});
} catch (error) {
next(error);
}
});
/**
* GET /models/:id/url - Get download URL for viewing
*/
router.get('/:id/url', validate(schemas.uuidParam, 'params'), async (req, res, next) => {
try {
const { id } = req.params;
const url = await modelsService.getModelDownloadUrl(id);
if (!url) {
throw new NotFoundError('Model not ready or not found');
}
res.json({
success: true,
data: { url },
});
} catch (error) {
next(error);
}
});
/**
* GET /models/:id/lod - Get all LOD URLs for a model
*/
router.get('/:id/lod', validate(schemas.uuidParam, 'params'), async (req, res, next) => {
try {
const { id } = req.params;
const lodUrls = await modelsService.getModelLodUrls(id);
if (!lodUrls) {
throw new NotFoundError('Model not ready or not found');
}
res.json({
success: true,
data: lodUrls,
});
} catch (error) {
next(error);
}
});
/**
* GET /models/:id/lod/:level - Get URL for specific LOD level
*/
router.get('/:id/lod/:level', validate(schemas.uuidParam, 'params'), async (req, res, next) => {
try {
const { id, level } = req.params;
const lodLevel = parseInt(level, 10);
if (isNaN(lodLevel) || lodLevel < 0 || lodLevel > 2) {
res.status(400).json({
success: false,
error: 'Invalid LOD level. Must be 0, 1, or 2.',
});
return;
}
const url = await modelsService.getModelLodUrl(id, lodLevel);
if (!url) {
throw new NotFoundError('Model not ready or not found');
}
res.json({
success: true,
data: { url, level: lodLevel },
});
} catch (error) {
next(error);
}
});
/**
* PATCH /models/:id - Update model metadata
*/
router.patch(
'/:id',
validate(schemas.uuidParam, 'params'),
validate(schemas.updateModel),
async (req, res, next) => {
try {
const { id } = req.params;
const updates = req.body;
const model = await modelsService.updateModel(id, updates);
if (!model) {
throw new NotFoundError('Model not found');
}
logger.info({ modelId: id }, 'Model updated');
res.json({
success: true,
data: model,
});
} catch (error) {
next(error);
}
}
);
/**
* DELETE /models/:id - Delete a model
*/
router.delete('/:id', validate(schemas.uuidParam, 'params'), async (req, res, next) => {
try {
const { id } = req.params;
const deleted = await modelsService.deleteModel(id);
if (!deleted) {
throw new NotFoundError('Model not found');
}
logger.info({ modelId: id }, 'Model deleted');
res.json({
success: true,
message: 'Model deleted successfully',
});
} catch (error) {
next(error);
}
});
/**
* POST /models/:id/thumbnail - Upload a thumbnail for a model
*/
router.post(
'/:id/thumbnail',
validate(schemas.uuidParam, 'params'),
upload.single('thumbnail'),
async (req, res, next) => {
try {
const { id } = req.params;
if (!req.file) {
res.status(400).json({
success: false,
error: 'No thumbnail file provided',
});
return;
}
const model = await modelsService.uploadThumbnail(id, req.file.buffer);
if (!model) {
throw new NotFoundError('Model not found');
}
logger.info({ modelId: id }, 'Thumbnail uploaded');
res.json({
success: true,
data: model,
thumbnail_url: model.thumbnail_url,
});
} catch (error) {
next(error);
}
}
);
/**
* POST /models/:id/regenerate-thumbnail - Regenerate thumbnail for a model
*/
router.post(
'/:id/regenerate-thumbnail',
validate(schemas.uuidParam, 'params'),
async (req, res, next) => {
try {
const { id } = req.params;
const model = await modelsService.getModelById(id);
if (!model) {
throw new NotFoundError('Model not found');
}
if (!model.model_url) {
res.status(400).json({
success: false,
error: 'Model not ready for thumbnail generation',
});
return;
}
await addThumbnailJob({ modelId: id, modelUrl: model.model_url });
logger.info({ modelId: id }, 'Thumbnail regeneration job queued');
res.json({
success: true,
message: 'Thumbnail regeneration job queued',
});
} catch (error) {
next(error);
}
}
);
export default router;