4ddcc1bCymbal simulator: shell-of-revolution modal model with a nonlinear energy cascade, numbl MATLAB reference, and interactive web appJeremy Magland 1// Hook owning the render worker: debounced auto-preview whenever the
2// parameters or the .m sources change, explicit full-quality render, and a
3// latest-wins request queue.
5import { useCallback, useEffect, useRef, useState } from 'react';
6import type { CymbalParams } from '../params';
7import type { MFile } from '../mfiles';
8import type { ErrorMessage, ProgressMessage, RenderResponse } from '../worker/renderWorker';
10export interface RenderError {
11 message: string;
12 file?: string;
13 line?: number;
14}
16export interface RenderState {
17 result: RenderResponse | null;
18 error: RenderError | null;
19 busy: boolean;
20 stage: string;
21 progress: number; // 0..1 of the current stage
22}
24interface Request {
25 params: CymbalParams;
26 mfiles: MFile[];
27 quality: 'preview' | 'full';
28}
30export function useRenderer(params: CymbalParams, mfiles: MFile[]) {
31 const workerRef = useRef<Worker | null>(null);
32 const nextId = useRef(1);
33 const inFlight = useRef<number | null>(null);
34 const queued = useRef<Request | null>(null);
35 const sendRef = useRef<(r: Request) => void>(() => {});
36 const [state, setState] = useState<RenderState>({
37 result: null,
38 error: null,
39 busy: false,
40 stage: '',
41 progress: 0,
42 });
44 const send = useCallback((req: Request) => {
45 const worker = workerRef.current;
46 if (!worker) return;
47 if (inFlight.current !== null) {
48 queued.current = req; // latest wins
49 return;
50 }
51 const id = nextId.current++;
52 inFlight.current = id;
53 setState((s) => ({ ...s, busy: true, stage: 'starting', progress: 0 }));
54 worker.postMessage({ type: 'render', id, ...req });
55 }, []);
56 sendRef.current = send;
58 useEffect(() => {
59 const worker = new Worker(new URL('../worker/renderWorker.ts', import.meta.url), {
60 type: 'module',
61 });
62 workerRef.current = worker;
64 const finish = (update: (s: RenderState) => RenderState) => {
65 inFlight.current = null;
66 const next = queued.current;
67 queued.current = null;
68 setState((s) => {
69 const done = update(s);
70 return next ? { ...done, busy: true, stage: 'queued', progress: 0 } : done;
71 });
72 if (next) sendRef.current(next);
73 };
75 worker.onmessage = (ev: MessageEvent<RenderResponse | ProgressMessage | ErrorMessage>) => {
76 const msg = ev.data;
77 if (msg.type === 'progress') {
78 setState((s) => ({ ...s, stage: msg.stage, progress: msg.frac }));
79 return;
80 }
81 if (msg.type === 'error') {
82 // Keep the last good result on screen; the code panel shows the fault.
83 finish((s) => ({
84 ...s,
85 error: { message: msg.message, file: msg.file, line: msg.line },
86 busy: false,
87 stage: '',
88 progress: 0,
89 }));
90 return;
91 }
92 finish(() => ({ result: msg, error: null, busy: false, stage: '', progress: 0 }));
93 };
94 return () => {
95 worker.terminate();
96 workerRef.current = null;
97 inFlight.current = null;
98 queued.current = null;
99 };
100 }, []);
102 const renderFull = useCallback(
103 () => send({ params, mfiles, quality: 'full' }),
104 [params, mfiles, send],
105 );
107 // Debounced auto-preview on any change to the parameters or the code.
108 useEffect(() => {
109 const t = setTimeout(() => send({ params, mfiles, quality: 'preview' }), 250);
110 return () => clearTimeout(t);
111 }, [params, mfiles, send]);
113 return { ...state, renderFull };
114}