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

175 lines
5.2 KiB
JavaScript

import { add, mul, sub, dot, cross, unit, point } from "./math.js";
export function triangleHit(mesh, f, origin, direction) {
const ids = mesh.indices.subarray(f * 3, f * 3 + 3);
const a = point(mesh.positions, ids[0]);
const e1 = sub(point(mesh.positions, ids[1]), a),
e2 = sub(point(mesh.positions, ids[2]), a);
const h = cross(direction, e2),
det = dot(e1, h);
if (Math.abs(det) < 1e-9) return null;
const s = sub(origin, a),
u = dot(s, h) / det;
if (u < 0 || u > 1) return null;
const q = cross(s, e1),
v = dot(direction, q) / det;
if (v < 0 || u + v > 1) return null;
const t = dot(e2, q) / det;
return t > 0
? {
t,
position: add(origin, mul(direction, t)),
normal: unit(cross(e1, e2)),
face: f,
}
: null;
}
export class BVH {
constructor(mesh) {
this.mesh = mesh;
this.leaves = new Array(mesh.indices.length / 3);
const centers = Array.from({ length: this.leaves.length }, (_, f) =>
[0, 1, 2].map(
(k) =>
(mesh.positions[mesh.indices[f * 3] * 3 + k] +
mesh.positions[mesh.indices[f * 3 + 1] * 3 + k] +
mesh.positions[mesh.indices[f * 3 + 2] * 3 + k]) /
3,
),
);
const build = (faces, parent = null) => {
const node = { parent, min: [0, 0, 0], max: [0, 0, 0] };
if (faces.length <= 8) {
node.faces = faces;
for (const f of faces) this.leaves[f] = node;
} else {
const spans = [0, 1, 2].map((k) => {
let lo = Infinity,
hi = -Infinity;
for (const f of faces) {
lo = Math.min(lo, centers[f][k]);
hi = Math.max(hi, centers[f][k]);
}
return hi - lo;
});
const axis = spans.indexOf(Math.max(...spans));
faces.sort((a, b) => centers[a][axis] - centers[b][axis]);
const mid = faces.length >> 1;
node.left = build(faces.slice(0, mid), node);
node.right = build(faces.slice(mid), node);
}
this.bounds(node);
return node;
};
this.root = this.leaves.length
? build(Array.from({ length: this.leaves.length }, (_, i) => i))
: null;
this.marks = new Uint32Array(mesh.positions.length / 3);
this.generation = 0;
}
bounds(node) {
for (let k = 0; k < 3; k++) {
let lo = Infinity,
hi = -Infinity;
if (node.faces)
for (const f of node.faces)
for (let j = 0; j < 3; j++) {
const v = this.mesh.positions[this.mesh.indices[f * 3 + j] * 3 + k];
lo = Math.min(lo, v);
hi = Math.max(hi, v);
}
else {
lo = Math.min(node.left.min[k], node.right.min[k]);
hi = Math.max(node.left.max[k], node.right.max[k]);
}
node.min[k] = lo;
node.max[k] = hi;
}
}
refit(faces) {
if (!this.root) return;
const dirty = new Set(faces.map((f) => this.leaves[f]));
for (const leaf of dirty) this.bounds(leaf);
let parents = new Set([...dirty].map((n) => n.parent).filter(Boolean));
while (parents.size) {
const next = new Set();
for (const n of parents) {
this.bounds(n);
if (n.parent) next.add(n.parent);
}
parents = next;
}
}
raycast(origin, direction) {
if (!this.root) return null;
let best = null;
const entry = (n) => {
let near = 0,
far = best?.t ?? Infinity;
for (let k = 0; k < 3; k++) {
if (Math.abs(direction[k]) < 1e-12) {
if (origin[k] < n.min[k] || origin[k] > n.max[k]) return Infinity;
continue;
}
const a = (n.min[k] - origin[k]) / direction[k],
b = (n.max[k] - origin[k]) / direction[k];
near = Math.max(near, Math.min(a, b));
far = Math.min(far, Math.max(a, b));
if (near > far) return Infinity;
}
return near;
};
const visit = (n) => {
if (entry(n) === Infinity) return;
if (n.faces) {
for (const f of n.faces) {
const hit = triangleHit(this.mesh, f, origin, direction);
if (hit && (!best || hit.t < best.t)) best = hit;
}
} else if (entry(n.left) < entry(n.right)) {
visit(n.left);
visit(n.right);
} else {
visit(n.right);
visit(n.left);
}
};
visit(this.root);
return best;
}
within(center, radius) {
if (!this.root) return [];
const result = [],
gen = ++this.generation,
r2 = radius * radius;
const visit = (n) => {
let d = 0;
for (let k = 0; k < 3; k++) {
const e = Math.max(n.min[k] - center[k], 0, center[k] - n.max[k]);
d += e * e;
}
if (d > r2) return;
if (n.faces)
for (const f of n.faces)
for (let j = 0; j < 3; j++) {
const v = this.mesh.indices[f * 3 + j];
if (this.marks[v] === gen) continue;
this.marks[v] = gen;
const p = this.mesh.positions;
const d2 =
(p[v * 3] - center[0]) ** 2 +
(p[v * 3 + 1] - center[1]) ** 2 +
(p[v * 3 + 2] - center[2]) ** 2;
if (d2 < r2) result.push(v);
}
else {
visit(n.left);
visit(n.right);
}
};
visit(this.root);
return result;
}
}