///
/**
* Runs a numbl filter script off the main thread so full-resolution images
* with per-pixel scripts don't freeze the page.
*
* Contract: the script defines a function (default name `filterImage`) that
* takes the image array `img` (H x W x 3 doubles, 0-255) and returns a new
* image array. If the script has no function header it is run as-is with
* `img` predefined and `out` read back.
*/
import { executeCode, RTV } from "numbl";
import type { RuntimeValue } from "numbl";
import {
rgbaToTensorData,
tensorToRaw,
extractFunctionName,
} from "./imageConvert.ts";
import type { FilterRequest, FilterResponse } from "./filterTypes.ts";
self.onmessage = (e: MessageEvent) => {
const { id, script, width, height, rgba } = e.data;
const logs: string[] = [];
try {
const { data, shape } = rgbaToTensorData(rgba, width, height);
const img = RTV.tensor(data, shape);
const fnName = extractFunctionName(script);
const source = fnName ? `out = ${fnName}(img);` : script;
const workspaceFiles = fnName
? [{ name: `${fnName}.m`, source: script }]
: [];
const t0 = performance.now();
const result = executeCode(
source,
{
initialVariableValues: { img },
optimization: "1", // JS-JIT; browser-safe
displayResults: false,
onOutput: (text: string) => logs.push(text),
},
workspaceFiles,
"main.m"
);
const elapsedMs = performance.now() - t0;
const out: RuntimeValue | undefined =
result.variableValues.out ?? result.returnValue;
const image = tensorToRaw(out);
const response: FilterResponse = {
id,
ok: true,
width: image.width,
height: image.height,
rgba: image.rgba,
logs,
elapsedMs,
};
self.postMessage(response, [image.rgba.buffer]);
} catch (err) {
const response: FilterResponse = {
id,
ok: false,
error: err instanceof Error ? err.message : String(err),
logs,
};
self.postMessage(response);
}
};