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>
175 lines
5.2 KiB
JavaScript
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;
|
|
}
|
|
}
|