printhive/lib/python.js
dlawler489 502eefdec2 Stream-parse 3MF files so Bambu projects don't exhaust memory
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>
2026-09-15 07:02:00 +10:00

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 };