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
+142
View File
@@ -0,0 +1,142 @@
import { unit } from "./math.js";
export function sphere(level = 5) {
if (!Number.isInteger(level) || level < 4 || level > 6)
throw new Error("Choose a supported resolution.");
const t = (1 + Math.sqrt(5)) / 2;
const vertices = [
[-1, t, 0],
[1, t, 0],
[-1, -t, 0],
[1, -t, 0],
[0, -1, t],
[0, 1, t],
[0, -1, -t],
[0, 1, -t],
[t, 0, -1],
[t, 0, 1],
[-t, 0, -1],
[-t, 0, 1],
].map(unit);
let faces = [
[0, 11, 5],
[0, 5, 1],
[0, 1, 7],
[0, 7, 10],
[0, 10, 11],
[1, 5, 9],
[5, 11, 4],
[11, 10, 2],
[10, 7, 6],
[7, 1, 8],
[3, 9, 4],
[3, 4, 2],
[3, 2, 6],
[3, 6, 8],
[3, 8, 9],
[4, 9, 5],
[2, 4, 11],
[6, 2, 10],
[8, 6, 7],
[9, 8, 1],
];
for (let l = 0; l < level; l++) {
const cache = new Map();
const midpoint = (a, b) => {
const key = Math.min(a, b) * 100000 + Math.max(a, b);
if (!cache.has(key)) {
cache.set(key, vertices.length);
vertices.push(unit(vertices[a].map((v, i) => v + vertices[b][i])));
}
return cache.get(key);
};
faces = faces.flatMap(([a, b, c]) => {
const ab = midpoint(a, b),
bc = midpoint(b, c),
ca = midpoint(c, a);
return [
[a, ab, ca],
[b, bc, ab],
[c, ca, bc],
[ab, bc, ca],
];
});
}
const positions = new Float32Array(vertices.flat());
const indices = new Uint32Array(faces.flat());
const incident = Array.from({ length: vertices.length }, () => []);
const neighbors = Array.from({ length: vertices.length }, () => new Set());
faces.forEach((face, f) =>
face.forEach((v) => {
incident[v].push(f);
for (const other of face) if (other !== v) neighbors[v].add(other);
}),
);
const mesh = {
level,
positions,
indices,
normals: new Float32Array(positions.length),
faceNormals: new Float32Array(indices.length),
incident: incident.map((a) => Uint32Array.from(a)),
neighbors: neighbors.map((a) => Uint32Array.from(a)),
};
mesh.faceMarks = new Uint32Array(faces.length);
mesh.vertexMarks = new Uint32Array(vertices.length);
mesh.generation = 0;
updateNormals(mesh);
return mesh;
}
// Include the unchanged incident faces when recomputing a boundary vertex normal.
export function updateNormals(mesh, moved) {
const { positions: p, indices: idx, normals: n, faceNormals: fn } = mesh;
const faces = [],
vertices = [];
const gen = ++mesh.generation;
if (moved) {
for (const v of moved)
for (const f of mesh.incident[v]) {
if (mesh.faceMarks[f] !== gen) {
mesh.faceMarks[f] = gen;
faces.push(f);
}
}
} else for (let f = 0; f < idx.length / 3; f++) faces.push(f);
for (const f of faces) {
const a = idx[f * 3],
b = idx[f * 3 + 1],
c = idx[f * 3 + 2];
const ax = p[b * 3] - p[a * 3],
ay = p[b * 3 + 1] - p[a * 3 + 1],
az = p[b * 3 + 2] - p[a * 3 + 2];
const bx = p[c * 3] - p[a * 3],
by = p[c * 3 + 1] - p[a * 3 + 1],
bz = p[c * 3 + 2] - p[a * 3 + 2];
fn[f * 3] = ay * bz - az * by;
fn[f * 3 + 1] = az * bx - ax * bz;
fn[f * 3 + 2] = ax * by - ay * bx;
for (let j = 0; j < 3; j++) {
const v = idx[f * 3 + j];
if (mesh.vertexMarks[v] !== gen) {
mesh.vertexMarks[v] = gen;
vertices.push(v);
}
}
}
for (const v of vertices) {
let x = 0,
y = 0,
z = 0;
for (const f of mesh.incident[v]) {
x += fn[f * 3];
y += fn[f * 3 + 1];
z += fn[f * 3 + 2];
}
const length = Math.hypot(x, y, z) || 1;
n[v * 3] = x / length;
n[v * 3 + 1] = y / length;
n[v * 3 + 2] = z / length;
}
return { faces, vertices };
}