trimesh's 3MF loader re-reads a whole production-extension part file for every component that points into it. A Bambu Studio project with 884 components across four ~50 MB part files grew past 3 GB within seconds and failed both the preview conversion and the geometry fingerprint. - Add tools/threemf_stream.py: parses each part once with lxml iterparse, resolves each component to the single object it names, and returns each mesh once plus its placements. The same project now converts in ~8 s at ~380 MB peak (fingerprinting ~190 MB). - convert_3mf.py and fingerprint.py use it; the GLB stores each object once and instances repeats. Stats are computed per placement (mirrored placements keep a positive volume), without concatenating meshes. - Run sidecar scripts through one queue (lib/python.js) so a preview and background fingerprinting never load big meshes at the same time; previews go first, and 3MFs are fingerprinted one per run. - Surface the converter's JSON error / timeout instead of "Command failed". - Tests with a Bambu-style component fixture (shared part, mirrored placement, decoy object) and STL/3MF fingerprint parity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
42 lines
1.3 KiB
JavaScript
42 lines
1.3 KiB
JavaScript
// Runs the Python/trimesh sidecar scripts one at a time. Loading a large mesh
|
|
// can take hundreds of MB, so a preview conversion and background
|
|
// fingerprinting never run at once. Jobs someone is waiting on (previews) go
|
|
// ahead of queued background work.
|
|
|
|
const path = require('path');
|
|
const { execFile } = require('child_process');
|
|
|
|
const PYTHON = process.env.PYTHON_BIN || path.join(__dirname, '..', 'tools', 'venv', 'bin', 'python');
|
|
const TOOLS = path.join(__dirname, '..', 'tools');
|
|
|
|
const urgent = [];
|
|
const background = [];
|
|
let busy = false;
|
|
|
|
// Resolves { err, stdout, stderr } — never rejects, so callers can use partial
|
|
// output from a run that failed part-way.
|
|
function runScript(script, args, { timeout = 120000, isBackground = false } = {}) {
|
|
return new Promise((resolve) => {
|
|
(isBackground ? background : urgent).push({ script, args, timeout, resolve });
|
|
next();
|
|
});
|
|
}
|
|
|
|
function next() {
|
|
if (busy) return;
|
|
const job = urgent.shift() || background.shift();
|
|
if (!job) return;
|
|
busy = true;
|
|
execFile(
|
|
PYTHON,
|
|
[path.join(TOOLS, job.script), ...job.args],
|
|
{ maxBuffer: 16 * 1024 * 1024, timeout: job.timeout },
|
|
(err, stdout, stderr) => {
|
|
busy = false;
|
|
job.resolve({ err, stdout: String(stdout || ''), stderr: String(stderr || '') });
|
|
next();
|
|
}
|
|
);
|
|
}
|
|
|
|
module.exports = { PYTHON, runScript };
|