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/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
+ }