A Bambu Studio or OrcaSlicer 3MF names the filaments its plates use, but reading that means shelling out to Python, so it can't be done per page load. A background pass — in the same loop as thumbnails and fingerprints — reads it once per model into models.filaments, which turns "what can I print now" into a comparison of two lists. Adds a Filament page for the spools on hand: material, colour, brand and grams left, edited in place. Matching is on material only; a project's exact colours would make almost nothing printable, and a different colour still prints. Measured on the dev library: 10 models read with no errors, the two Bambu projects resolving to four PLA colours each, and no plate images written because the reader skips --images and queues behind anything interactive. Not done yet: nothing in the library view uses this — no "printable now" badge or filter — and the Filament page itself has not been seen rendered, because the local preview browser has no session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
3.3 KiB
JavaScript
85 lines
3.3 KiB
JavaScript
// What each model asks for, and what you have on hand.
|
|
//
|
|
// A Bambu Studio / OrcaSlicer 3MF names the filaments its plates use. Reading
|
|
// that means shelling out to Python, so it is done once per model in the
|
|
// background — like thumbnails and fingerprints — and kept in models.filaments.
|
|
// With that stored, "what can I print right now" is a plain query rather than
|
|
// hundreds of subprocesses.
|
|
const db = require('./db');
|
|
const converter = require('./converter');
|
|
|
|
const BATCH = 25;
|
|
|
|
// Models that have never been looked at, paired with their first loose 3MF.
|
|
// A model whose only 3MF sits inside an archive is left for a later pass rather
|
|
// than paying an extraction for each one.
|
|
const PENDING = `
|
|
FROM models m
|
|
LEFT JOIN LATERAL (
|
|
SELECT f.file_path FROM files f
|
|
WHERE f.model_id = m.id AND f.format = '3mf'
|
|
ORDER BY f.id LIMIT 1
|
|
) f3 ON true
|
|
WHERE m.deleted_at IS NULL AND m.filaments_status IS NULL`;
|
|
|
|
// "PLA Basic" and "pla basic" are the same filament; "PLA-CF" is not.
|
|
const normalise = (s) => String(s || '').trim().toUpperCase().replace(/\s+/g, ' ');
|
|
|
|
// The materials a model needs that nothing on hand provides. Colour is ignored
|
|
// on purpose: matching a project's exact colours would make almost nothing
|
|
// printable, and a different colour still prints.
|
|
function missingMaterials(filaments, spools) {
|
|
const onHand = new Set(spools
|
|
.filter((s) => s.remaining_g === null || s.remaining_g === undefined || Number(s.remaining_g) > 0)
|
|
.map((s) => normalise(s.material)));
|
|
const needed = [...new Set((filaments || []).map((f) => normalise(f.type)).filter(Boolean))];
|
|
return needed.filter((type) => !onHand.has(type));
|
|
}
|
|
|
|
// True when every filament the model names is on a spool you still have.
|
|
// A model we know nothing about is not claimed to be printable.
|
|
function isPrintable(filaments, spools) {
|
|
if (!filaments || !filaments.length) return false;
|
|
return missingMaterials(filaments, spools).length === 0;
|
|
}
|
|
|
|
// One pass. Returns whether it did any work, so it fits the background worker
|
|
// loop in fingerprint.js.
|
|
async function run() {
|
|
const { rows } = await db.query(
|
|
`SELECT m.id, f3.file_path ${PENDING} ORDER BY m.id LIMIT ${BATCH}`);
|
|
if (!rows.length) return false;
|
|
|
|
for (const row of rows) {
|
|
if (!row.file_path) {
|
|
await mark(row.id, 'none', null);
|
|
continue;
|
|
}
|
|
try {
|
|
const filaments = await converter.readFilaments(row.file_path);
|
|
// A plain 3MF mesh names no filaments — that is an answer, not a failure.
|
|
await mark(row.id, filaments.length ? 'done' : 'none', filaments.length ? filaments : null);
|
|
} catch (err) {
|
|
console.warn(`Could not read filaments for model ${row.id}:`, err.message);
|
|
await mark(row.id, 'error', null);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function mark(modelId, status, filaments) {
|
|
await db.query('UPDATE models SET filaments = $2, filaments_status = $3 WHERE id = $1',
|
|
[modelId, filaments ? JSON.stringify(filaments) : null, status]);
|
|
}
|
|
|
|
async function pendingCount() {
|
|
const { rows } = await db.query(`SELECT COUNT(*)::int AS n ${PENDING}`);
|
|
return rows[0].n;
|
|
}
|
|
|
|
// Look at every model again — after adding files, say.
|
|
async function reread() {
|
|
await db.query('UPDATE models SET filaments_status = NULL');
|
|
}
|
|
|
|
module.exports = { run, pendingCount, reread, missingMaterials, isPrintable, normalise };
|