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:
Amp
2026-09-28 08:53:50 +00:00
co-authored by heaust
parent eb8aa0e9dc
commit 347b676cf1
25 changed files with 5468 additions and 1 deletions
+217
View File
@@ -0,0 +1,217 @@
import test from "node:test";
import assert from "node:assert/strict";
import { Worker } from "node:worker_threads";
import { Mailbox } from "./mailbox.js";
import { StrokeProcessor } from "./stroke-worker.js";
import { Sculpt, defaults, StrokePath } from "./engine.js";
import { sphere } from "./mesh.js";
const row = () => ({
buffer: new SharedArrayBuffer(128),
descriptor: { offset: 64 },
});
test("mailbox overwrites one slot, keeps a newer publication pending, wraps and rejects torn reads", (t) => {
const m = new Mailbox(row()),
p = { x: 12, y: -8, pressure: 0.5 };
const first = m.write(7, p);
assert.deepEqual(m.read(7), { ...p, seed: undefined, version: first });
const second = m.write(7, { x: 20, y: 3, pressure: 0.75 });
m.acknowledge(first);
assert.equal(m.read(7).version, second);
m.acknowledge(second);
assert.equal(m.read(7), null);
Atomics.store(m.words, 0, 0x7ffffffe);
const wrapped = m.write(7, p);
assert.equal(wrapped, -2147483648);
assert.equal(m.read(7).x, 12);
const load = Atomics.load;
let changed = false;
t.mock.method(Atomics, "load", (array, index) => {
const v = load(array, index);
if (array === m.words && index === 5 && !changed) {
changed = true;
m.write(7, { x: 99, y: 100, pressure: 0.2 });
}
return v;
});
assert.equal(m.read(7), null);
Atomics.store(m.words, 0, 11);
assert.equal(m.read(7), null);
});
test("actual concurrent SAB writer and reader never observe mixed coordinates", async (t) => {
const shared = row(),
m = new Mailbox(shared),
control = new Int32Array(shared.buffer, 0, 1);
const code = `import {parentPort,workerData} from 'node:worker_threads';
const {Mailbox}=await import(workerData.module);
const m=new Mailbox(workerData.row),control=new Int32Array(workerData.row.buffer,0,1);
let reads=0,last=0;
parentPort.postMessage('ready');
while(!Atomics.load(control,0)||last!==100000){
const p=m.read(7);if(!p)continue;
if(p.y!==-p.x||p.pressure!==p.x*2||p.seed!==p.x*3)throw new Error('torn tuple');
last=p.x;reads++;m.acknowledge(p.version);
}parentPort.postMessage({reads,last});`;
const worker = new Worker(
new URL("data:text/javascript," + encodeURIComponent(code)),
{
workerData: {
module: new URL("./mailbox.js", import.meta.url).href,
row: shared,
},
},
);
t.after(() => worker.terminate());
await new Promise((resolve, reject) => {
worker.once("message", resolve);
worker.once("error", reject);
});
const result = new Promise((resolve, reject) => {
worker.once("message", resolve);
worker.once("error", reject);
});
for (let i = 1; i <= 100000; i++) {
m.write(7, { x: i, y: -i, pressure: i * 2 }, i * 3);
if (i % 1000 === 0) await new Promise((resolve) => setImmediate(resolve));
}
Atomics.store(control, 0, 1);
const out = await result;
assert.ok(out.reads > 0);
assert.equal(out.last, 100000);
});
function setup(settings = {}, sample = { x: 400, y: 300, pressure: 0.7 }) {
const mailbox = new Mailbox(row()),
messages = [],
mesh = sphere(4);
let now = 0;
const processor = new StrokeProcessor(
mesh,
mailbox,
(m) => messages.push(m),
() => now,
);
const version = mailbox.write(1, sample);
processor.begin({
id: 1,
version,
sample,
positions: mesh.positions.slice(),
settings: { ...defaults, symmetry: false, ...settings },
camera: { yaw: 0, pitch: 0, zoom: 1.5, target: [0, 0, 0] },
rect: { width: 800, height: 600 },
hit: { position: [0, 0, 1], normal: [0, 0, 1] },
});
const ack = () => {
for (const m of messages)
if (m.type === "geometry") processor.acknowledge(m.packet);
};
return {
processor,
mailbox,
messages,
ack,
advance: (ms) => {
now += ms;
},
};
}
test("latest frame target equals feeding only that target, intermediate samples create no hidden queue", () => {
const a = setup(),
b = setup();
a.processor.frame();
b.processor.frame();
a.ack();
b.ack();
for (let i = 0; i < 10000; i++)
a.mailbox.write(1, {
x: 410 + i / 100,
y: 300 + Math.sin(i) * 30,
pressure: 0.4,
});
const end = { x: 470, y: 310, pressure: 0.8 };
const version = a.mailbox.write(1, end);
b.mailbox.write(1, end);
a.processor.frame();
b.processor.frame();
assert.deepEqual(
a.processor.engine.mesh.positions,
b.processor.engine.mesh.positions,
);
assert.equal(a.mailbox.processed, version);
assert.equal(a.processor.job.target, null);
});
test("Grab seeds pressure-radius from first effective pressure and ignores a zero-pressure tail", () => {
const a = setup(
{ brush: "grab", pressureRadius: true },
{ x: 400, y: 300, pressure: 0 },
);
a.processor.frame();
const version = a.mailbox.write(1, { x: 460, y: 310, pressure: 0.8 }, 0.25);
a.processor.frame();
const expected = new Sculpt(sphere(4));
expected.begin({ brush: "grab", pressureRadius: true, symmetry: false });
expected.dab({ position: [0, 0, 1], normal: [0, 0, 1] }, 0.25, [0, 0, 0]);
expected.dab(
{ position: [0, 0, 1], normal: [0, 0, 1] },
Math.fround(0.8),
[0.3, -0.05, 0],
);
assert.deepEqual(a.processor.engine.mesh.positions, expected.mesh.positions);
assert.equal(a.mailbox.processed, version);
});
test("one immutable output in flight, new dirty work survives acknowledgement, end waits for applied output", () => {
const a = setup();
a.processor.frame();
const first = a.messages.find((m) => m.type === "geometry"),
bytes = first.vertices.slice();
const version = a.mailbox.write(1, { x: 470, y: 310, pressure: 0.9 });
a.processor.end(1, version);
a.processor.frame();
assert.deepEqual(first.vertices, bytes);
assert.equal(a.messages.filter((m) => m.type === "geometry").length, 1);
assert.ok(a.processor.engine.dirty.size);
assert.ok(!a.messages.some((m) => m.type === "complete"));
a.processor.acknowledge(first.packet);
a.processor.frame();
const second = a.messages.filter((m) => m.type === "geometry").at(-1);
assert.notEqual(second.packet, first.packet);
a.processor.acknowledge(first.packet);
assert.equal(a.processor.outstanding, second.packet);
a.processor.acknowledge(second.packet);
a.processor.frame();
assert.equal(a.messages.at(-1).type, "complete");
});
test("worker bounds real work; unfinished segment age is not reset by release or newer samples", () => {
const a = setup();
a.processor.frame();
a.ack();
const dab = a.processor.engine.dab.bind(a.processor.engine);
let calls = 0;
a.processor.engine.dab = (...args) => {
calls++;
a.advance(5);
dab(...args);
};
a.mailbox.write(1, { x: 590, y: 300, pressure: 0.8 });
a.processor.frame();
assert.equal(calls, 1);
assert.ok(a.processor.job.target);
a.advance(250);
const latest = a.mailbox.write(1, { x: 610, y: 305, pressure: 0.9 });
a.processor.end(1, latest);
a.processor.frame();
assert.equal(a.processor.job, null);
assert.equal(a.messages.at(-1).type, "canceled");
});
test("resuming a segment preserves independently computed positions and pressure", () => {
const emitted = [],
path = new StrokePath(5, (p) => emitted.push(p));
path.push({ x: 0, y: 0, pressure: 0.2 });
while (!path.push({ x: 24, y: 18, pressure: 0.8 }, 1)) {}
assert.equal(emitted.length, 7);
emitted.forEach((p, i) => {
assert.ok(Math.abs(p.x - 4 * i) < 1e-10);
assert.ok(Math.abs(p.y - 3 * i) < 1e-10);
assert.ok(Math.abs(p.pressure - (0.2 + 0.1 * i)) < 1e-10);
});
});