Live console + progress bars — Engine.log + Engine.progress

Stream log lines and progress bars to the UI / CLI **while the script runs**, not just after it finishes. `Engine.log("…")` appears in the Console panel the instant it's called; an `Engine.progress("Encoding", { total: 40 })` handle drives an inline progress bar that ticks up every `p.step()`. Multiple handles can be active simultaneously — each gets its own bar. Three patterns shown: 1. **Indeterminate progress** (spinner-style) for work where you don't know the unit count up front. `.message("…")` updates the status text without changing a value. 2. **Determinate progress** (filled bar) for loops with a known total. `.step(delta, msg)` advances and optionally updates the status line. 3. **Multiple parallel progresses** — declare as many handles as you need; the UI stacks them inline. Pair this with batch mode or long-running ML tools and the user finally sees what the script is doing.

INPUT
INPUT — Live console + progress bars — Engine.log + Engine.progress
Hue-cycle montage
Hue-cycle montage — Live console + progress bars — Engine.log + Engine.progress
JavaScript
// Live console + progress bars — Engine.log() + Engine.progress()
// stream straight to the v3 UI's console panel and the CLI's
// indicatif renderer as work happens. No more waiting for the
// script to finish before output appears.
// demo_progress.js
//!INPUT: INPUT
//!OUTPUT: OUTPUT
//!PARAM: FRAMES:integer=12,min=4,max=60

const src = Engine.loadImage(INPUT);

// 1) Indeterminate progress — no `total`, just a "still working"
//    spinner. The UI shows a moving pulse; the CLI shows a spinner.
const setup = Engine.progress("Preparing");
setup.message("loading reference frame");
const reference = src.clone().resize(256, 256);
setup.message("computing hue cycle");
setup.done("ready");

// 2) Determinate progress — the bar fills from 0 to FRAMES. Each
//    step() call carries a status message that the UI / CLI shows
//    next to the percentage.
const pass = Engine.progress("Animating hue cycle", { total: FRAMES });
const frames = [];
for (let i = 0; i < FRAMES; i++) {
    const t = i / (FRAMES - 1);
    const frame = reference.clone()
        .hue(t * 360)
        .saturation(1 + 0.5 * Math.sin(t * Math.PI));
    frames.push(frame);
    Engine.log(`frame ${i + 1}/${FRAMES} — hue=${Math.round(t * 360)}°`);
    pass.step(1, `frame ${i + 1}/${FRAMES}`);
}
pass.done(`✓ ${FRAMES} frames rendered`);

// 3) A second determinate progress, in parallel with the first if
//    the script were async — here it just shows that multiple
//    handles can be active independently.
// One montageH() call with all frames as args allocates a single
// output handle — avoids the N-1 intermediates a fold loop would
// leak. The progress bar ticks for visual feedback even though the
// actual stitch is one engine call.
const stitch = Engine.progress("Stitching montage", { total: FRAMES });
for (let i = 1; i < FRAMES; i++) {
    stitch.step(1, `column ${i + 1}/${FRAMES}`);
}
const row = frames[0].montageH(...frames.slice(1));
stitch.done("✓ montage ready");

row.save(OUTPUT);
Engine.log("done — saved to OUTPUT");

// Cleanup — free every frame + the reference. The original src too.
for (const f of frames) f.free();
reference.free();
row.free();
src.free();

// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0