import { Mailbox } from "./mailbox.js"; import { pressureValue } from "./engine.js"; export class StrokeClient { constructor(row, level, onError) { this.mailbox = new Mailbox(row.share()); this.worker = new Worker(new URL("./stroke-worker.js", import.meta.url), { type: "module", name: "sculpt-samples", }); this.nextId = 0; this.onError = onError; this.ready = new Promise((resolve, reject) => { this.resolveReady = resolve; this.rejectReady = reject; }); this.timer = setTimeout( () => this.fail(new Error("Sculpt worker initialization timed out")), 20000, ); this.worker.onmessage = ({ data }) => this.message(data); this.worker.onerror = () => this.fail(new Error("Sculpt worker stopped")); this.worker.onmessageerror = () => this.fail(new Error("Sculpt worker message failed")); this.worker.postMessage({ type: "init", row: row.share(), level }); } begin(data, apply) { if (this.job || this.disposed) throw new Error("Sculpt worker is unavailable"); const id = ++this.nextId; const promise = new Promise((resolve, reject) => { this.job = { id, settings: data.settings, apply, resolve, reject }; }); this.write(data.sample); this.worker.postMessage( { type: "begin", ...data, id, version: this.version }, [data.positions.buffer], ); return promise; } write(sample) { const j = this.job; if (!j || j.canceling) return; if (j.settings.brush === "grab") { if ( pressureValue(sample.pressure, j.settings.gain, j.settings.gamma) === 0 && this.version !== undefined && j.hasSample ) return; if ( pressureValue(sample.pressure, j.settings.gain, j.settings.gamma) > 0 && j.seed === undefined ) j.seed = sample.pressure; } this.version = this.mailbox.write(j.id, sample, j.seed); j.hasSample = true; } end() { if (!this.job || this.job.canceling) return; this.worker.postMessage({ type: "end", id: this.job.id, version: this.version, }); this.armTimeout(); } cancel() { if (!this.job) return; this.job.canceling = true; this.worker.postMessage({ type: "cancel", id: this.job.id }); this.armTimeout(); } armTimeout() { clearTimeout(this.timer); this.timer = setTimeout( () => this.fail(new Error("Sculpt worker did not settle")), 10000, ); } message(data) { if (this.disposed) return; if (data.type === "ready") { clearTimeout(this.timer); this.resolveReady(); return; } if (data.type === "error") { this.fail(new Error(data.message)); return; } const j = this.job; if (data.type === "geometry") { if (j?.id === data.id && !j.canceling) j.apply(data); this.worker.postMessage({ type: "ack", packet: data.packet }); } else if ( j?.id === data.id && ["complete", "canceled"].includes(data.type) ) { clearTimeout(this.timer); this.job = null; j.resolve({ canceled: j.canceling || data.type === "canceled", message: data.message, }); } } fail(error) { if (this.disposed) return; this.rejectReady(error); this.job?.reject(error); this.job = null; this.dispose(); this.onError(error); } dispose() { this.disposed = true; clearTimeout(this.timer); this.worker.terminate(); this.rejectReady(new Error("Sculpt worker disposed")); this.job?.resolve({ canceled: true }); this.job = null; } }