feat: double executor to use with preview

This commit is contained in:
2026-09-04 18:17:35 +05:00
parent a19d3db929
commit 520a1e1d37
9 changed files with 201 additions and 698 deletions
+136
View File
@@ -0,0 +1,136 @@
import { ToolError } from "$lib/core/errors";
import type { PixelImage } from "$lib/core/types";
type MaybeRunnable = {
id: string;
domOnly?: boolean;
run?: (
img: PixelImage,
params: Record<string, unknown>,
) => Promise<PixelImage> | PixelImage;
};
export async function executeStep(
tool: MaybeRunnable,
img: PixelImage,
params: Record<string, unknown>,
): Promise<PixelImage> {
if (!tool.run) {
throw new Error("errors.noImageRun");
}
if (tool.domOnly) {
return await runDirect(tool, img, params);
}
if (typeof Worker === "undefined") {
return await runDirect(tool, img, params);
}
const worker = ensureWorker();
if (worker === null) {
return await runDirect(tool, img, params);
}
try {
return await runInWorker(worker, tool.id, img, params);
} catch (workerError) {
disableWorker();
void workerError;
return await runDirect(tool, img, params);
}
}
async function runDirect(
tool: MaybeRunnable,
img: PixelImage,
params: Record<string, unknown>,
): Promise<PixelImage> {
if (!tool.run) {
throw new Error("errors.noImageRun");
}
return await tool.run(img, params);
}
let worker: Worker | null = null;
let workerTried = false;
let nextRequestId = 1;
const pending = new Map<
number,
{ resolve: (img: PixelImage) => void; reject: (e: Error) => void }
>();
function ensureWorker(): Worker | null {
if (workerTried) return worker;
workerTried = true;
try {
const candidate = new Worker(
new URL("./executor.worker.ts", import.meta.url),
{
type: "module",
},
);
candidate.onmessage = (event: MessageEvent) => {
const payload = event.data as {
id: number;
ok: boolean;
width?: number;
height?: number;
data?: Uint8ClampedArray;
error?: string;
errorKey?: string;
errorVars?: Record<string, string | number>;
};
const entry = pending.get(payload.id);
if (!entry) return;
pending.delete(payload.id);
if (payload.ok && payload.data && payload.width && payload.height) {
entry.resolve({
width: payload.width,
height: payload.height,
data: new Uint8ClampedArray(payload.data),
});
} else if (payload.errorKey) {
entry.reject(new ToolError(payload.errorKey, payload.errorVars));
} else {
entry.reject(new Error(payload.error ?? "errors.workerFailed"));
}
};
candidate.onerror = () => {
worker = null;
for (const entry of pending.values()) {
entry.reject(new Error("errors.workerUnavailable"));
}
pending.clear();
};
worker = candidate;
} catch {
worker = null;
}
return worker;
}
function disableWorker(): void {
if (worker) {
worker.terminate();
}
worker = null;
}
function runInWorker(
workerInstance: Worker,
toolId: string,
img: PixelImage,
params: Record<string, unknown>,
): Promise<PixelImage> {
return new Promise((resolve, reject) => {
const id = nextRequestId++;
pending.set(id, { resolve, reject });
workerInstance.postMessage({
id,
toolId,
image: {
width: img.width,
height: img.height,
data: new Uint8ClampedArray(img.data),
},
params,
});
});
}
@@ -0,0 +1,51 @@
/// <reference lib="webworker" />
import { ToolError } from "$lib/core/errors";
import type { PixelImage } from "$lib/core/types";
import { getTool } from "$lib/registry-new";
import { sanitizeSchemaParams } from "$lib/registry-schema";
type WorkerRequest = {
id: number;
toolId: string;
image: { width: number; height: number; data: Uint8ClampedArray };
params: Record<string, unknown>;
};
self.onmessage = (event: MessageEvent<WorkerRequest>) => {
void handle(event.data);
};
async function handle(request: WorkerRequest): Promise<void> {
try {
const tool = getTool(request.toolId);
if (!tool?.run) {
throw new Error("errors.noImageRun");
}
const image: PixelImage = {
width: request.image.width,
height: request.image.height,
data: new Uint8ClampedArray(request.image.data),
};
const output = await tool.run(
image,
sanitizeSchemaParams(tool.schema, request.params),
);
const payload = {
id: request.id,
ok: true,
width: output.width,
height: output.height,
data: output.data,
};
(self as unknown as Worker).postMessage(payload, [output.data.buffer]);
} catch (e) {
const toolError = e instanceof ToolError ? e : undefined;
(self as unknown as Worker).postMessage({
id: request.id,
ok: false,
error: e instanceof Error ? e.message : String(e),
errorKey: toolError?.key,
errorVars: toolError?.vars,
});
}
}
+1
View File
@@ -0,0 +1 @@
export { executeStep } from "./executor";
-193
View File
@@ -1,193 +0,0 @@
import type { ToolEntry } from "$lib/registry";
export type SegmentedField = {
id: string;
kind: "segmented";
label: string;
options: string[];
selected: number;
};
export type ColorPairField = {
id: string;
kind: "color-pair";
label: string;
from: string;
to: string;
};
export type SliderField = {
id: string;
kind: "slider" | "direction";
label: string;
value: number;
min: number;
max: number;
suffix: string;
space?: boolean;
hints?: [string, string];
};
export type ColorField = {
id: string;
kind: "color";
label: string;
value: string;
native?: boolean;
reference?: boolean;
};
export type ToggleField = {
id: string;
kind: "toggle";
label: string;
checked: boolean;
note: string;
};
export type FieldDef =
SegmentedField | ColorPairField | SliderField | ColorField | ToggleField;
export interface PreviewToolView {
title: string;
lede: string;
panelLabel: string;
panelStrong: string;
stepType: string;
toolbarLabel: string;
fileName: string;
downloadLabel: string;
layoutClass: string;
previewClass: string;
preview: "gradient" | "comparison";
fields: FieldDef[];
}
export type FieldValue =
number | string | boolean | { from: string; to: string };
function initValue(f: FieldDef): FieldValue {
switch (f.kind) {
case "segmented":
return f.selected;
case "color-pair":
return { from: f.from, to: f.to };
case "color":
return f.value;
case "slider":
case "direction":
return f.value;
case "toggle":
return f.checked;
}
}
export function initForm(view: PreviewToolView): Record<string, FieldValue> {
const form: Record<string, FieldValue> = {};
for (const f of view.fields) form[f.id] = initValue(f);
return form;
}
const gradient: PreviewToolView = {
title: "Gradient background.",
lede: "Create a clean, export-ready gradient with precise control over color, direction and transparency.",
panelLabel: "GRADIENT SETTINGS",
panelStrong: "Configure output",
stepType: "TOOL 01",
toolbarLabel: "OUTPUT PREVIEW",
fileName: "gradient.png",
downloadLabel: "Download PNG",
layoutClass: "gradient-layout",
previewClass: "gradient-preview",
preview: "gradient",
fields: [
{
id: "type",
kind: "segmented",
label: "GRADIENT TYPE",
options: ["Linear", "Radial"],
selected: 0,
},
{
id: "stops",
kind: "color-pair",
label: "COLOR STOPS",
from: "#1769D2",
to: "#00A8C7",
},
{
id: "dir",
kind: "direction",
label: "DIRECTION",
value: 135,
min: 0,
max: 360,
suffix: "°",
space: true,
},
{
id: "op",
kind: "slider",
label: "OPACITY",
value: 100,
min: 0,
max: 100,
suffix: "%",
space: true,
},
],
};
const background: PreviewToolView = {
title: "Remove background.",
lede: "Select a background color and tune the edge detection. Changes are processed automatically in your browser.",
panelLabel: "REMOVER SETTINGS",
panelStrong: "Configure detection",
stepType: "TOOL 02",
toolbarLabel: "SOURCE / RESULT",
fileName: "comparison.png",
downloadLabel: "Download result",
layoutClass: "remover-layout",
previewClass: "remover-preview",
preview: "comparison",
fields: [
{
id: "color",
kind: "color",
label: "BACKGROUND COLOR",
value: "#E8EEF2",
native: true,
reference: true,
},
{
id: "sim",
kind: "slider",
label: "COLOR SIMILARITY",
value: 72,
min: 0,
max: 100,
suffix: "%",
space: true,
hints: ["strict edges", "more removal"],
},
{
id: "outer",
kind: "toggle",
label: "OUTER COLOR ONLY",
checked: true,
note: "Only remove connected background pixels from the edges.",
},
{
id: "mask",
kind: "toggle",
label: "SHOW MASK",
checked: false,
note: "Preview the detected transparency mask.",
},
],
};
export const PREVIEW_TOOL_VIEWS: Record<string, PreviewToolView> = {
"linear-gradient-png": gradient,
"remove-background-png": background,
};
export function getPreviewView(tool: ToolEntry): PreviewToolView | undefined {
return PREVIEW_TOOL_VIEWS[tool.id];
}