Add core-powered web sculpting studio with tablet controls
Add pressure-aware worker sculpting, bounded shared input, project files and history, and compact icon navigation with an XYZ orbit gizmo. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83 Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user