Self-hosted Express + Postgres app for organizing STL/3MF/OBJ/etc. with: - Model/file library, tags, collections, print settings - URL import: Thingiverse (API), Printables (GraphQL), MakerWorld (FlareSolverr + token), cubee3d/other (metadata + manual files) - In-browser 3D viewer; 3MF rendered via Python/trimesh GLB conversion - Auto thumbnails, license/commercial-use tracking, in-app editing - Settings (API tokens / FlareSolverr) stored in DB with .env fallback Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
60 lines
2.1 KiB
JavaScript
60 lines
2.1 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const NodeStl = require('node-stl');
|
|
const AdmZip = require('adm-zip');
|
|
|
|
const THUMBNAIL_DIR = './thumbnails';
|
|
|
|
const MODEL_FORMATS = ['stl', '3mf', 'obj', 'step', 'stp', 'gcode', 'gco', 'zip'];
|
|
|
|
function ensureThumbnailDir() {
|
|
if (!fs.existsSync(THUMBNAIL_DIR)) fs.mkdirSync(THUMBNAIL_DIR, { recursive: true });
|
|
}
|
|
|
|
function formatOf(filename) {
|
|
return path.extname(filename).slice(1).toLowerCase();
|
|
}
|
|
|
|
function isAllowed(filename) {
|
|
return MODEL_FORMATS.includes(formatOf(filename));
|
|
}
|
|
|
|
// Geometry metadata. Only STL is computed natively today; others return nulls
|
|
// (3MF/OBJ/STEP volume can be added later via a Python/trimesh sidecar).
|
|
function extractMetadata(filePath, format) {
|
|
if (format !== 'stl') return { volume: null, bbox: null, facets: null };
|
|
try {
|
|
const stl = new NodeStl(filePath);
|
|
const bb = stl.boundingBox; // [x, y, z]
|
|
return {
|
|
volume: Number.isFinite(stl.volume) ? stl.volume : null,
|
|
bbox: Array.isArray(bb) ? { x: bb[0], y: bb[1], z: bb[2] } : null,
|
|
facets: null, // node-stl does not expose facet count
|
|
};
|
|
} catch (err) {
|
|
console.error('STL metadata extraction failed:', err.message);
|
|
return { volume: null, bbox: null, facets: null };
|
|
}
|
|
}
|
|
|
|
// A .3mf is a zip; slicers embed a PNG preview under /Metadata/. If present,
|
|
// pull it out as the model thumbnail. Returns a path under thumbnails/ or null.
|
|
function extract3mfThumbnail(filePath, baseName) {
|
|
try {
|
|
ensureThumbnailDir();
|
|
const zip = new AdmZip(filePath);
|
|
const entry = zip.getEntries().find((e) => {
|
|
const n = e.entryName.toLowerCase();
|
|
return n.endsWith('.png') && (n.includes('thumbnail') || n.includes('/metadata/'));
|
|
});
|
|
if (!entry) return null;
|
|
const out = path.join(THUMBNAIL_DIR, `${baseName}.png`);
|
|
fs.writeFileSync(out, entry.getData());
|
|
return out;
|
|
} catch (err) {
|
|
console.error('3MF thumbnail extraction failed:', err.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
module.exports = { MODEL_FORMATS, formatOf, isAllowed, extractMetadata, extract3mfThumbnail };
|