// Hook owning the render worker: debounced auto-preview whenever the // parameters or the .m sources change, explicit full-quality render, and a // latest-wins request queue. import { useCallback, useEffect, useRef, useState } from 'react'; import type { WoodblockParams } from '../params'; import type { MFile } from '../mfiles'; import type { ErrorMessage, ProgressMessage, RenderResponse } from '../worker/renderWorker'; export interface RenderError { message: string; file?: string; line?: number; } export interface RenderState { result: RenderResponse | null; error: RenderError | null; busy: boolean; stage: string; progress: number; // 0..1 of the current stage } interface Request { params: WoodblockParams; mfiles: MFile[]; quality: 'preview' | 'full'; } export function useRenderer(params: WoodblockParams, mfiles: MFile[]) { const workerRef = useRef(null); const nextId = useRef(1); const inFlight = useRef(null); const queued = useRef(null); const sendRef = useRef<(r: Request) => void>(() => {}); const [state, setState] = useState({ result: null, error: null, busy: false, stage: '', progress: 0, }); const send = useCallback((req: Request) => { const worker = workerRef.current; if (!worker) return; if (inFlight.current !== null) { queued.current = req; // latest wins return; } const id = nextId.current++; inFlight.current = id; setState((s) => ({ ...s, busy: true, stage: 'starting', progress: 0 })); worker.postMessage({ type: 'render', id, ...req }); }, []); sendRef.current = send; useEffect(() => { const worker = new Worker(new URL('../worker/renderWorker.ts', import.meta.url), { type: 'module', }); workerRef.current = worker; const finish = (update: (s: RenderState) => RenderState) => { inFlight.current = null; const next = queued.current; queued.current = null; setState((s) => { const done = update(s); return next ? { ...done, busy: true, stage: 'queued', progress: 0 } : done; }); if (next) sendRef.current(next); }; worker.onmessage = (ev: MessageEvent) => { const msg = ev.data; if (msg.type === 'progress') { setState((s) => ({ ...s, stage: msg.stage, progress: msg.frac })); return; } if (msg.type === 'error') { // Keep the last good result on screen; the code panel shows the fault. finish((s) => ({ ...s, error: { message: msg.message, file: msg.file, line: msg.line }, busy: false, stage: '', progress: 0, })); return; } finish(() => ({ result: msg, error: null, busy: false, stage: '', progress: 0 })); }; return () => { worker.terminate(); workerRef.current = null; inFlight.current = null; queued.current = null; }; }, []); const renderFull = useCallback( () => send({ params, mfiles, quality: 'full' }), [params, mfiles, send], ); // Debounced auto-preview on any change to the parameters or the code. useEffect(() => { const t = setTimeout(() => send({ params, mfiles, quality: 'preview' }), 250); return () => clearTimeout(t); }, [params, mfiles, send]); return { ...state, renderFull }; }