Files
Ampandheaust d279ec4123 Add clay pieces, masking, and topology editing to sculpt
Add pastel presets, piece selection and colors, mask brushes, subdivision and conservative decimation with bounded worker operations and mixed undo history.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-10-01 07:55:48 +00:00

168 lines
4.6 KiB
JavaScript

import { unit } from "./math.js";
export function buildMesh(positions, indices, level = 5, masks) {
positions =
positions instanceof Float32Array
? positions
: new Float32Array(positions.flat ? positions.flat(Infinity) : positions);
indices =
indices instanceof Uint32Array
? indices
: new Uint32Array(indices.flat ? indices.flat(Infinity) : indices);
if (positions.length % 3 || indices.length % 3)
throw new Error(
"Mesh positions and indices must contain complete triples.",
);
const vertexCount = positions.length / 3;
const incident = Array.from({ length: vertexCount }, () => []);
const neighbors = Array.from({ length: vertexCount }, () => new Set());
for (let f = 0; f < indices.length / 3; f++) {
const face = indices.subarray(f * 3, f * 3 + 3);
for (const v of face) {
if (v >= vertexCount) throw new Error("Mesh index is out of range.");
incident[v].push(f);
for (const other of face) if (other !== v) neighbors[v].add(other);
}
}
if (masks && masks.length !== vertexCount)
throw new Error("Mask count must match vertex count.");
const mesh = {
level,
positions,
indices,
masks:
masks instanceof Float32Array
? masks
: new Float32Array(masks || vertexCount),
normals: new Float32Array(positions.length),
faceNormals: new Float32Array(indices.length),
incident: incident.map((a) => Uint32Array.from(a)),
neighbors: neighbors.map((a) => Uint32Array.from(a)),
faceMarks: new Uint32Array(indices.length / 3),
vertexMarks: new Uint32Array(vertexCount),
generation: 0,
};
updateNormals(mesh);
return mesh;
}
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());
return buildMesh(positions, indices, level);
}
// 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 };
}