spine-rigc 1.2.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/cli.ts +124 -20
- package/docs/AUTHORING.md +471 -19
- package/docs/FACE.md +15 -0
- package/docs/INGEST.md +1 -1
- package/package.json +17 -1
- package/src/atlas.ts +151 -2
- package/src/compile.ts +335 -0
- package/src/mesh.ts +405 -1
- package/src/pose.ts +297 -10
- package/src/render.ts +376 -21
- package/src/rig.ts +140 -7
- package/src/types.ts +30 -0
- package/src/validate.ts +101 -24
package/src/mesh.ts
CHANGED
|
@@ -99,7 +99,7 @@ export interface MeshVertexWeight {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/** Which builder in this file made a mesh's geometry. */
|
|
102
|
-
export type MeshKind = 'ring' | 'ribbon' | 'contour' | 'grid';
|
|
102
|
+
export type MeshKind = 'ring' | 'ribbon' | 'contour' | 'grid' | 'segments';
|
|
103
103
|
|
|
104
104
|
export interface MeshGeometry {
|
|
105
105
|
kind: MeshKind;
|
|
@@ -1795,3 +1795,407 @@ export function meshEdges(vertexCount: number, triangles: readonly number[], hul
|
|
|
1795
1795
|
for (const [a, b] of interior) out.push(2 * a, 2 * b);
|
|
1796
1796
|
return out;
|
|
1797
1797
|
}
|
|
1798
|
+
|
|
1799
|
+
// ---------------------------------------------------------------------------
|
|
1800
|
+
// segments — a lattice over the part's alpha, weighted by distance to bones
|
|
1801
|
+
// ---------------------------------------------------------------------------
|
|
1802
|
+
//
|
|
1803
|
+
// The generator for "an arbitrary layer pulled by a chosen set of bones". Its
|
|
1804
|
+
// one authored input is WHICH segments may pull the part; the geometry is read
|
|
1805
|
+
// off the art and the weights off the distance between the two, so a pipeline
|
|
1806
|
+
// that used to re-implement distance weighting outside the compiler states the
|
|
1807
|
+
// segment list and nothing else.
|
|
1808
|
+
//
|
|
1809
|
+
// The algorithm is a port of `spine-parts`' lattice mesh and segment weights
|
|
1810
|
+
// (MIT, same owner). What it does, in order:
|
|
1811
|
+
//
|
|
1812
|
+
// 1. **Cells.** A square lattice of `cell`-pixel squares over the part; the
|
|
1813
|
+
// last column and row are clipped to the image. A cell is kept when any
|
|
1814
|
+
// pixel inside it reaches the alpha threshold, so the triangles cover
|
|
1815
|
+
// every art pixel by construction.
|
|
1816
|
+
// 2. **One loop.** Spine's `hull` is one closed outline, and a layer is often
|
|
1817
|
+
// two islands or has a hole. So the kept cells are made one simply
|
|
1818
|
+
// connected region: holes filled, every other island bridged to the
|
|
1819
|
+
// largest by a straight run of cells, every diagonal pinch filled, and the
|
|
1820
|
+
// three repeated until a pass changes nothing. Every pass that does not
|
|
1821
|
+
// settle adds at least one cell, so the loop is bounded by the lattice and
|
|
1822
|
+
// needs no pass limit of its own. The added cells hold no art.
|
|
1823
|
+
// 3. **Triangles.** Vertices numbered in the order the kept cells first touch
|
|
1824
|
+
// them (cells row-major; corners top-left, top-right, bottom-right,
|
|
1825
|
+
// bottom-left), each cell two triangles with the diagonal alternating by
|
|
1826
|
+
// `(i + j) % 2`, so the lattice has no preferred shear. Emitted
|
|
1827
|
+
// counter-clockwise in Spine world, the winding every generator here
|
|
1828
|
+
// writes (GR02 holds the grid to it).
|
|
1829
|
+
// 4. **Outline first.** The boundary is walked from the edges used by exactly
|
|
1830
|
+
// one triangle, in first-seen order, and every interior vertex follows in
|
|
1831
|
+
// index order — the arrangement `checkHullOrder` requires.
|
|
1832
|
+
// 5. **Weights.** Per vertex and per candidate segment, `w = 1 / (d + r)^p`
|
|
1833
|
+
// where `d` is the distance to the segment's nearest point; a bone two
|
|
1834
|
+
// segments name keeps the larger; the strongest `maxBones` are kept and
|
|
1835
|
+
// normalised, a share under `minWeight` is dropped, and the rest are
|
|
1836
|
+
// normalised again. Ties keep the order the bones first appear in.
|
|
1837
|
+
|
|
1838
|
+
/** What the lattice step did, for the report and for the refusals. */
|
|
1839
|
+
export interface SegmentsLatticeReport {
|
|
1840
|
+
/** Cells across and down. */
|
|
1841
|
+
cols: number;
|
|
1842
|
+
rows: number;
|
|
1843
|
+
/** Cells holding an art pixel, and cells kept after the one-loop passes. */
|
|
1844
|
+
artCells: number;
|
|
1845
|
+
keptCells: number;
|
|
1846
|
+
/** Passes of hole-fill, island-join and pinch-fill until one changed nothing. */
|
|
1847
|
+
passes: number;
|
|
1848
|
+
/** 4-connected islands of art cells before the joins. */
|
|
1849
|
+
islands: number;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
export interface SegmentsLattice {
|
|
1853
|
+
/** Vertex positions in part-local pixels, y down, outline first. */
|
|
1854
|
+
points: Array<[number, number]>;
|
|
1855
|
+
uvs: number[];
|
|
1856
|
+
/** Counter-clockwise in Spine world. */
|
|
1857
|
+
triangles: number[];
|
|
1858
|
+
hullVertices: number;
|
|
1859
|
+
report: SegmentsLatticeReport;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/** `np.rint`: to the nearest integer, exact halves to even — the bridge's rounding. */
|
|
1863
|
+
function rintEven(x: number): number {
|
|
1864
|
+
const f = Math.floor(x);
|
|
1865
|
+
const frac = x - f;
|
|
1866
|
+
if (frac < 0.5) return f;
|
|
1867
|
+
if (frac > 0.5) return f + 1;
|
|
1868
|
+
return f % 2 === 0 ? f : f + 1;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
/** Fill every background cell the lattice border cannot reach through 4-connected background. */
|
|
1872
|
+
function fillCellHoles(cells: Uint8Array, nx: number, ny: number): Uint8Array {
|
|
1873
|
+
const outside = new Uint8Array(nx * ny);
|
|
1874
|
+
const stack: number[] = [];
|
|
1875
|
+
const seed = (i: number): void => {
|
|
1876
|
+
if (cells[i] === 0 && outside[i] === 0) {
|
|
1877
|
+
outside[i] = 1;
|
|
1878
|
+
stack.push(i);
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
for (let x = 0; x < nx; x++) {
|
|
1882
|
+
seed(x);
|
|
1883
|
+
seed((ny - 1) * nx + x);
|
|
1884
|
+
}
|
|
1885
|
+
for (let y = 0; y < ny; y++) {
|
|
1886
|
+
seed(y * nx);
|
|
1887
|
+
seed(y * nx + nx - 1);
|
|
1888
|
+
}
|
|
1889
|
+
while (stack.length > 0) {
|
|
1890
|
+
const i = stack.pop()!;
|
|
1891
|
+
const x = i % nx;
|
|
1892
|
+
const y = (i - x) / nx;
|
|
1893
|
+
if (x > 0) seed(i - 1);
|
|
1894
|
+
if (x < nx - 1) seed(i + 1);
|
|
1895
|
+
if (y > 0) seed(i - nx);
|
|
1896
|
+
if (y < ny - 1) seed(i + nx);
|
|
1897
|
+
}
|
|
1898
|
+
const out = new Uint8Array(nx * ny);
|
|
1899
|
+
for (let i = 0; i < out.length; i++) out[i] = outside[i] === 1 ? 0 : 1;
|
|
1900
|
+
return out;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* 4-connected components of the kept cells, numbered in raster order of each
|
|
1905
|
+
* component's first cell, each with its cells in raster order.
|
|
1906
|
+
*/
|
|
1907
|
+
function cellIslands(cells: Uint8Array, nx: number, ny: number): Array<Array<[number, number]>> {
|
|
1908
|
+
const label = new Int32Array(nx * ny).fill(-1);
|
|
1909
|
+
const islands: Array<Array<[number, number]>> = [];
|
|
1910
|
+
for (let start = 0; start < cells.length; start++) {
|
|
1911
|
+
if (cells[start] === 0 || label[start] >= 0) continue;
|
|
1912
|
+
const id = islands.length;
|
|
1913
|
+
const stack = [start];
|
|
1914
|
+
label[start] = id;
|
|
1915
|
+
while (stack.length > 0) {
|
|
1916
|
+
const i = stack.pop()!;
|
|
1917
|
+
const x = i % nx;
|
|
1918
|
+
const y = (i - x) / nx;
|
|
1919
|
+
const visit = (j: number): void => {
|
|
1920
|
+
if (cells[j] !== 0 && label[j] < 0) {
|
|
1921
|
+
label[j] = id;
|
|
1922
|
+
stack.push(j);
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
if (x > 0) visit(i - 1);
|
|
1926
|
+
if (x < nx - 1) visit(i + 1);
|
|
1927
|
+
if (y > 0) visit(i - nx);
|
|
1928
|
+
if (y < ny - 1) visit(i + nx);
|
|
1929
|
+
}
|
|
1930
|
+
islands.push([]);
|
|
1931
|
+
}
|
|
1932
|
+
for (let i = 0; i < cells.length; i++) if (label[i] >= 0) islands[label[i]].push([(i - (i % nx)) / nx, i % nx]);
|
|
1933
|
+
return islands;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
/**
|
|
1937
|
+
* The lattice over one part's alpha: cells, one loop, triangles, outline first.
|
|
1938
|
+
*
|
|
1939
|
+
* Refuses a part that keeps no cell and a cell that is not a whole number of
|
|
1940
|
+
* pixels of at least 1; everything else about the geometry follows from the art.
|
|
1941
|
+
*/
|
|
1942
|
+
export function buildSegmentsLattice(input: { mask: AlphaMask; threshold: number; cell: number }): SegmentsLattice {
|
|
1943
|
+
const { mask, threshold, cell } = input;
|
|
1944
|
+
const { width: w, height: h } = mask;
|
|
1945
|
+
if (!Number.isInteger(cell) || cell < 1) {
|
|
1946
|
+
throw new MeshError(`"cell" is ${JSON.stringify(cell)}; it is the lattice's square, a whole number of pixels of at least 1`);
|
|
1947
|
+
}
|
|
1948
|
+
if (!Number.isInteger(threshold) || threshold < 1 || threshold > 255) {
|
|
1949
|
+
throw new MeshError(`the alpha threshold must be a whole number in 1..255, got ${threshold}`);
|
|
1950
|
+
}
|
|
1951
|
+
const nx = Math.ceil(w / cell);
|
|
1952
|
+
const ny = Math.ceil(h / cell);
|
|
1953
|
+
const xs = Array.from({ length: nx + 1 }, (_, i) => Math.min(i * cell, w));
|
|
1954
|
+
const ys = Array.from({ length: ny + 1 }, (_, j) => Math.min(j * cell, h));
|
|
1955
|
+
let cells: Uint8Array = new Uint8Array(nx * ny);
|
|
1956
|
+
let artCells = 0;
|
|
1957
|
+
for (let j = 0; j < ny; j++) {
|
|
1958
|
+
for (let i = 0; i < nx; i++) {
|
|
1959
|
+
let any = 0;
|
|
1960
|
+
for (let y = ys[j]; y < ys[j + 1] && any === 0; y++) {
|
|
1961
|
+
for (let x = xs[i]; x < xs[i + 1]; x++) {
|
|
1962
|
+
if (mask.alpha[y * w + x] >= threshold) {
|
|
1963
|
+
any = 1;
|
|
1964
|
+
break;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
cells[j * nx + i] = any;
|
|
1969
|
+
artCells += any;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
if (artCells === 0) {
|
|
1973
|
+
throw new MeshError(
|
|
1974
|
+
`no pixel of the ${w}x${h} part reaches alpha ${threshold}, so a lattice at cell ${cell} keeps no cell — ` +
|
|
1975
|
+
'there is no art to cover. Lower "alpha", or point at the image this mesh is meant to draw',
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
const islands = cellIslands(cells, nx, ny).length;
|
|
1979
|
+
|
|
1980
|
+
// 2. one loop, until a pass changes nothing.
|
|
1981
|
+
let passes = 0;
|
|
1982
|
+
for (;;) {
|
|
1983
|
+
passes++;
|
|
1984
|
+
cells = fillCellHoles(cells, nx, ny);
|
|
1985
|
+
const found = cellIslands(cells, nx, ny);
|
|
1986
|
+
if (found.length > 1) {
|
|
1987
|
+
let main = 0;
|
|
1988
|
+
for (let k = 1; k < found.length; k++) if (found[k].length > found[main].length) main = k;
|
|
1989
|
+
const mainCells = found[main];
|
|
1990
|
+
for (let k = 0; k < found.length; k++) {
|
|
1991
|
+
if (k === main) continue;
|
|
1992
|
+
const other = found[k];
|
|
1993
|
+
let best = Infinity;
|
|
1994
|
+
let a = 0;
|
|
1995
|
+
let b = 0;
|
|
1996
|
+
for (let p = 0; p < other.length; p++) {
|
|
1997
|
+
for (let q = 0; q < mainCells.length; q++) {
|
|
1998
|
+
const dj = other[p][0] - mainCells[q][0];
|
|
1999
|
+
const di = other[p][1] - mainCells[q][1];
|
|
2000
|
+
const d = dj * dj + di * di;
|
|
2001
|
+
if (d < best) {
|
|
2002
|
+
best = d;
|
|
2003
|
+
a = p;
|
|
2004
|
+
b = q;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
const [j0, i0] = other[a];
|
|
2009
|
+
const [j1, i1] = mainCells[b];
|
|
2010
|
+
const steps = 2 * (Math.abs(j1 - j0) + Math.abs(i1 - i0)) + 2;
|
|
2011
|
+
const stride = 1 / (steps - 1);
|
|
2012
|
+
for (let s = 0; s < steps; s++) {
|
|
2013
|
+
const t = s === steps - 1 ? 1 : s * stride;
|
|
2014
|
+
cells[rintEven(j0 + (j1 - j0) * t) * nx + i0] = 1;
|
|
2015
|
+
cells[j1 * nx + rintEven(i0 + (i1 - i0) * t)] = 1;
|
|
2016
|
+
cells[rintEven(j0 + (j1 - j0) * t) * nx + rintEven(i0 + (i1 - i0) * t)] = 1;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
const snap = new Uint8Array(cells);
|
|
2022
|
+
const at = (j: number, i: number): number => snap[j * nx + i];
|
|
2023
|
+
let pinch = false;
|
|
2024
|
+
for (let j = 0; j < ny - 1; j++) {
|
|
2025
|
+
for (let i = 0; i < nx - 1; i++) {
|
|
2026
|
+
if (at(j, i) && at(j + 1, i + 1) && !at(j, i + 1) && !at(j + 1, i)) {
|
|
2027
|
+
cells[j * nx + i + 1] = 1;
|
|
2028
|
+
pinch = true;
|
|
2029
|
+
} else if (at(j, i + 1) && at(j + 1, i) && !at(j, i) && !at(j + 1, i + 1)) {
|
|
2030
|
+
cells[j * nx + i] = 1;
|
|
2031
|
+
pinch = true;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
if (!pinch) break;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// 3. triangles, in the lattice's own winding (top-left, top-right, bottom-right).
|
|
2039
|
+
const vid = new Int32Array((nx + 1) * (ny + 1)).fill(-1);
|
|
2040
|
+
const pts: Array<[number, number]> = [];
|
|
2041
|
+
const tri: number[] = [];
|
|
2042
|
+
let keptCells = 0;
|
|
2043
|
+
for (let j = 0; j < ny; j++) {
|
|
2044
|
+
for (let i = 0; i < nx; i++) {
|
|
2045
|
+
if (cells[j * nx + i] === 0) continue;
|
|
2046
|
+
keptCells++;
|
|
2047
|
+
const c: number[] = [];
|
|
2048
|
+
for (const [jj, ii] of [
|
|
2049
|
+
[j, i],
|
|
2050
|
+
[j, i + 1],
|
|
2051
|
+
[j + 1, i + 1],
|
|
2052
|
+
[j + 1, i],
|
|
2053
|
+
]) {
|
|
2054
|
+
const k = jj * (nx + 1) + ii;
|
|
2055
|
+
if (vid[k] < 0) {
|
|
2056
|
+
vid[k] = pts.length;
|
|
2057
|
+
pts.push([xs[ii], ys[jj]]);
|
|
2058
|
+
}
|
|
2059
|
+
c.push(vid[k]);
|
|
2060
|
+
}
|
|
2061
|
+
if ((i + j) % 2 === 0) tri.push(c[0], c[1], c[2], c[0], c[2], c[3]);
|
|
2062
|
+
else tri.push(c[0], c[1], c[3], c[1], c[2], c[3]);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// 4. the outline, walked from the boundary edges in first-seen order.
|
|
2067
|
+
const edgeUses = new Map<number, { a: number; b: number; n: number }>();
|
|
2068
|
+
const V = pts.length;
|
|
2069
|
+
for (let t = 0; t < tri.length; t += 3) {
|
|
2070
|
+
for (const [p, q] of [
|
|
2071
|
+
[tri[t], tri[t + 1]],
|
|
2072
|
+
[tri[t + 1], tri[t + 2]],
|
|
2073
|
+
[tri[t + 2], tri[t]],
|
|
2074
|
+
]) {
|
|
2075
|
+
const a = Math.min(p, q);
|
|
2076
|
+
const b = Math.max(p, q);
|
|
2077
|
+
const key = a * V + b;
|
|
2078
|
+
const e = edgeUses.get(key);
|
|
2079
|
+
if (e === undefined) edgeUses.set(key, { a, b, n: 1 });
|
|
2080
|
+
else e.n++;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
const adj = new Map<number, number[]>();
|
|
2084
|
+
const link = (p: number, q: number): void => {
|
|
2085
|
+
const list = adj.get(p);
|
|
2086
|
+
if (list === undefined) adj.set(p, [q]);
|
|
2087
|
+
else list.push(q);
|
|
2088
|
+
};
|
|
2089
|
+
for (const { a, b, n } of edgeUses.values()) {
|
|
2090
|
+
if (n !== 1) continue;
|
|
2091
|
+
link(a, b);
|
|
2092
|
+
link(b, a);
|
|
2093
|
+
}
|
|
2094
|
+
const order: number[] = [];
|
|
2095
|
+
const seen = new Set<number>();
|
|
2096
|
+
for (const s of adj.keys()) {
|
|
2097
|
+
if (seen.has(s)) continue;
|
|
2098
|
+
let cur = s;
|
|
2099
|
+
let prev = -1;
|
|
2100
|
+
while (!seen.has(cur)) {
|
|
2101
|
+
seen.add(cur);
|
|
2102
|
+
order.push(cur);
|
|
2103
|
+
const next = (adj.get(cur) ?? []).filter((v) => v !== prev && !seen.has(v));
|
|
2104
|
+
if (next.length === 0) break;
|
|
2105
|
+
prev = cur;
|
|
2106
|
+
cur = next[0];
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
const hullVertices = order.length;
|
|
2110
|
+
for (let v = 0; v < V; v++) if (!seen.has(v)) order.push(v);
|
|
2111
|
+
const remap = new Int32Array(V);
|
|
2112
|
+
order.forEach((v, i) => (remap[v] = i));
|
|
2113
|
+
const points = order.map((v) => pts[v]);
|
|
2114
|
+
// Emitted counter-clockwise in Spine world: the lattice's corners were taken
|
|
2115
|
+
// clockwise on the y-down page, so each triangle's last two are swapped.
|
|
2116
|
+
const triangles: number[] = [];
|
|
2117
|
+
for (let t = 0; t < tri.length; t += 3) triangles.push(remap[tri[t]], remap[tri[t + 2]], remap[tri[t + 1]]);
|
|
2118
|
+
const uvs: number[] = [];
|
|
2119
|
+
for (const [x, y] of points) uvs.push(r6(x / w), r6(y / h));
|
|
2120
|
+
return {
|
|
2121
|
+
points,
|
|
2122
|
+
uvs,
|
|
2123
|
+
triangles,
|
|
2124
|
+
hullVertices,
|
|
2125
|
+
report: { cols: nx, rows: ny, artCells, keptCells, passes, islands },
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/** One candidate segment, in Spine world, and which of the mesh's bones it belongs to. */
|
|
2130
|
+
export interface WorldSegment {
|
|
2131
|
+
/** Index into the mesh's distinct bone list. */
|
|
2132
|
+
bone: number;
|
|
2133
|
+
a: readonly [number, number];
|
|
2134
|
+
b: readonly [number, number];
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
/** The four numbers the falloff reads, every one of them from the spec. */
|
|
2138
|
+
export interface SegmentsFalloff {
|
|
2139
|
+
power: number;
|
|
2140
|
+
radius: number;
|
|
2141
|
+
maxBones: number;
|
|
2142
|
+
minWeight: number;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
/**
|
|
2146
|
+
* Distance from `p` to the segment `a -> b`, a zero-length segment being its point.
|
|
2147
|
+
*
|
|
2148
|
+
* `sqrt(dx*dx + dy*dy)` rather than `Math.hypot`, whose last bit can differ:
|
|
2149
|
+
* a weight is written on a 6-decimal grid, and that is where one ulp becomes a
|
|
2150
|
+
* different number in the file.
|
|
2151
|
+
*/
|
|
2152
|
+
export function segmentDistance(p: readonly [number, number], a: readonly [number, number], b: readonly [number, number]): number {
|
|
2153
|
+
const abx = b[0] - a[0];
|
|
2154
|
+
const aby = b[1] - a[1];
|
|
2155
|
+
const len2 = abx * abx + aby * aby;
|
|
2156
|
+
let t = 0;
|
|
2157
|
+
if (len2 > 0) {
|
|
2158
|
+
t = ((p[0] - a[0]) * abx + (p[1] - a[1]) * aby) / len2;
|
|
2159
|
+
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
2160
|
+
}
|
|
2161
|
+
const dx = p[0] - (a[0] + t * abx);
|
|
2162
|
+
const dy = p[1] - (a[1] + t * aby);
|
|
2163
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
/**
|
|
2167
|
+
* The shares on one vertex, strongest first and closing at exactly 1 on the
|
|
2168
|
+
* generator's 6-decimal grid — or, when every share the kept bones carry is
|
|
2169
|
+
* under `minWeight`, the unrounded shares that were dropped, for the refusal to
|
|
2170
|
+
* print. A vertex is never given a bone the falloff did not choose.
|
|
2171
|
+
*/
|
|
2172
|
+
export function segmentShares(
|
|
2173
|
+
p: readonly [number, number],
|
|
2174
|
+
segments: readonly WorldSegment[],
|
|
2175
|
+
falloff: SegmentsFalloff,
|
|
2176
|
+
): { weights: MeshVertexWeight[] } | { dropped: Array<{ bone: number; share: number }> } {
|
|
2177
|
+
const byBone = new Map<number, number>();
|
|
2178
|
+
for (const s of segments) {
|
|
2179
|
+
const reach = segmentDistance(p, s.a, s.b) + falloff.radius;
|
|
2180
|
+
const w = 1 / Math.pow(reach, falloff.power);
|
|
2181
|
+
const was = byBone.get(s.bone);
|
|
2182
|
+
byBone.set(s.bone, was === undefined ? w : Math.max(was, w));
|
|
2183
|
+
}
|
|
2184
|
+
// `sort` is stable, so equal pulls keep the order the bones first appeared in.
|
|
2185
|
+
const top = [...byBone.entries()].sort((x, y) => y[1] - x[1]).slice(0, falloff.maxBones);
|
|
2186
|
+
let sum = 0;
|
|
2187
|
+
for (const [, v] of top) sum += v;
|
|
2188
|
+
const shares = top.map(([bone, v]) => ({ bone, share: v / sum }));
|
|
2189
|
+
const kept = shares.filter((e) => e.share >= falloff.minWeight);
|
|
2190
|
+
if (kept.length === 0) return { dropped: shares };
|
|
2191
|
+
let sum2 = 0;
|
|
2192
|
+
for (const e of kept) sum2 += e.share;
|
|
2193
|
+
const weights: MeshVertexWeight[] = [];
|
|
2194
|
+
let others = 0;
|
|
2195
|
+
kept.forEach((e, k) => {
|
|
2196
|
+
const weight = k === kept.length - 1 ? r6(1 - others) : r6(e.share / sum2);
|
|
2197
|
+
others += weight;
|
|
2198
|
+
weights.push({ bone: 'control', control: e.bone, weight });
|
|
2199
|
+
});
|
|
2200
|
+
return { weights };
|
|
2201
|
+
}
|