Proportional editing weights
32-bit in partFor every vertex, how much of the selected vertices' move it follows: 1 for the selected ones, falling to 0 at the radius.
t = distance ÷ radius; smooth: x²(3 − 2x) with x = 1 − t; sphere: √(1 − t²); root: √x; linear: x; sharp: x²; constant: 1
The distance to the nearest selected vertex is kept in a Float64Array and the falloff is evaluated in 64-bit, but each weight is written to a Float32Array, which rounds it to about 7 digits. Selected vertices get exactly 1. The sphere curve clamps 1 − t² at 0 before the square root.
/**
* Proportional editing: how much each vertex follows a move of the selected ones (1 for them, falling to 0 at `radius`):
* by straight distance, or along edges when `connected`. → Float32Array per vertex slot (0 = stays).
*/
export function falloffWeights(m, sel, radius, { mode = 'smooth', connected = false } = {}) {
const W = new Float32Array(m.nv), D = new Float64Array(m.nv).fill(Infinity);
const f = (t) => { const x = 1 - t; switch (mode) { case 'sphere': return Math.sqrt(Math.max(0, 1 - t * t)); case 'root': return Math.sqrt(x); case 'linear': return x; case 'sharp': return x * x; case 'constant': return 1; default: return x * x * (3 - 2 * x); } };
const S = [...new Set(sel)];
for (const v of S) { D[v] = 0; W[v] = 1; }
if (!(radius > 0)) return W;
if (connected) {
// shortest distance along edges (a plain heap of [distance, vertex])
const heap = S.map((v) => [0, v]);
const pop = () => { let bi = 0; for (let i = 1; i < heap.length; i++) if (heap[i][0] < heap[bi][0]) bi = i; return heap.splice(bi, 1)[0]; };
while (heap.length) {
const [d, v] = pop();
if (d > D[v] || d > radius) continue;
for (const h of m.outgoing(v)) {
const w = m.head(h), nd = d + len(sub(P3(m, w), P3(m, v)));
if (nd < D[w] && nd <= radius) { D[w] = nd; heap.push([nd, w]); }
}
}
} else {
// straight distance to the nearest selected vertex (a grid of the selected ones)
const cell = radius, grid = new Map(), key = (x, y, z) => x + ',' + y + ',' + z;
for (const v of S) { const c = P3(m, v).map((x) => Math.floor(x / cell)); const k = key(...c); if (!grid.has(k)) grid.set(k, []); grid.get(k).push(v); }
for (let v = 0; v < m.nv; v++) {
if (m.vertDead(v) || D[v] === 0) continue;
const p = P3(m, v), c = p.map((x) => Math.floor(x / cell));
for (let dx = -1; dx <= 1; dx++) for (let dy = -1; dy <= 1; dy++) for (let dz = -1; dz <= 1; dz++) for (const s of grid.get(key(c[0] + dx, c[1] + dy, c[2] + dz)) || []) { const d = len(sub(P3(m, s), p)); if (d < D[v]) D[v] = d; }
}
}
for (let v = 0; v < m.nv; v++) if (D[v] > 0 && D[v] <= radius) W[v] = f(D[v] / radius);
return W;
}