spine-rigc 2.17.1 → 2.19.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/package.json +1 -1
- package/src/areaband.ts +96 -0
- package/src/deformsurvey.ts +5 -80
- package/src/mesh.ts +39 -8
- package/src/meshquality.ts +1571 -0
- package/src/meshreduce.ts +1294 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/areaband.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The area band a triangle's sign is read against — the one A39 reads a
|
|
3
|
+
* winding by, and the one the mesh-quality measurement reads orientation and
|
|
4
|
+
* degeneracy by (`src/meshquality.ts`).
|
|
5
|
+
*
|
|
6
|
+
* Moved here unchanged from `src/deformsurvey.ts`, which imports these back
|
|
7
|
+
* and re-exports every name it exported before, so no caller changed an import.
|
|
8
|
+
* They moved because a module the `spine-rigc/mesh` entry reaches has to read
|
|
9
|
+
* the same band, and `src/deformsurvey.ts` reaches the compiler and the core
|
|
10
|
+
* (`src/deformstructure.ts`, `src/core/`) — a geometry entry that loaded the
|
|
11
|
+
* compiler to read three numbers would also close an import cycle through
|
|
12
|
+
* `src/compile.ts`, which imports `src/mesh.ts`.
|
|
13
|
+
*
|
|
14
|
+
* Imports nothing: no clock, no runtime, no other module.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How near zero a triangle's area has to be, as a fraction of the largest
|
|
19
|
+
* triangle in the same mesh at the same pose, before a sign is not read off it.
|
|
20
|
+
*
|
|
21
|
+
* A RELATIVE band, because an absolute one has no scale that means anything on
|
|
22
|
+
* its own — these are pixel² figures on whatever plate the rig was drawn at, and
|
|
23
|
+
* `gallery/flex`'s leaf tops out at 792.6 px² where `spineboy-pro`'s hoverboard
|
|
24
|
+
* reaches 3338.4 px². On those two it comes to 7.9e-4 and 3.3e-3 px².
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ It is **not** what holds the float32 noise off; `float32AreaNoise` is, and
|
|
27
|
+
* the two are combined rather than ranked because on a big mesh the noise bound
|
|
28
|
+
* is the larger of them. This one is the shape band: it keeps a setup triangle
|
|
29
|
+
* that has no area from being read as a reversal of anything, and a triangle the
|
|
30
|
+
* key collapses onto zero from being read as turned over.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFORM_AREA_EPSILON = 1e-6;
|
|
33
|
+
|
|
34
|
+
/** Half an ulp of a float32 mantissa — the relative error of one stored coordinate. */
|
|
35
|
+
const FLOAT32_HALF_ULP = 2 ** -24;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* An upper bound on how much of a triangle's signed area is float32 noise.
|
|
39
|
+
*
|
|
40
|
+
* The world vertices arrive in a `Float32Array`, so each coordinate carries up
|
|
41
|
+
* to `|c|·2⁻²⁴` of error. An area is `½·(Δx₁·Δy₂ − Δx₂·Δy₁)`, and propagating
|
|
42
|
+
* that error through one product gives `Δ·|c|·2⁻²⁴` twice over; four such terms
|
|
43
|
+
* across the two products, halved, bounds the area error by `2·C²·2⁻²⁴` with `C`
|
|
44
|
+
* the largest coordinate magnitude in the mesh (which also bounds every `Δ`).
|
|
45
|
+
* Doubled once more for the subtraction, so the constant is 4.
|
|
46
|
+
*
|
|
47
|
+
* On a mesh whose vertices reach 500 units that is 6e-2 px², i.e. **larger** than
|
|
48
|
+
* the relative band above — which is the whole reason this exists. It is a bound
|
|
49
|
+
* rather than a measurement, and deliberately loose: what has to stay clear of it
|
|
50
|
+
* is a genuine reversal, and the smallest one anywhere in the corpus is
|
|
51
|
+
* `spineboy-pro`'s hoverboard triangle at 8.478 px², more than two orders of
|
|
52
|
+
* magnitude above. Nothing measured lands between the two, so nothing between
|
|
53
|
+
* them is being tuned.
|
|
54
|
+
*/
|
|
55
|
+
export function float32AreaNoise(world: ArrayLike<number>): number {
|
|
56
|
+
let coordinate = 0;
|
|
57
|
+
for (let i = 0; i < world.length; i++) coordinate = Math.max(coordinate, Math.abs(world[i]));
|
|
58
|
+
return 4 * coordinate * coordinate * FLOAT32_HALF_ULP;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Twice-signed area, halved, of every triangle of `triangles` over the
|
|
63
|
+
* interleaved `x, y` world vertices in `world`.
|
|
64
|
+
*
|
|
65
|
+
* The SIGN is the whole point and the magnitude is the tolerance's yardstick, so
|
|
66
|
+
* this returns the signed figure rather than an absolute one. Positive and
|
|
67
|
+
* negative are not "correct" and "wrong" — a mesh may be wound either way, and
|
|
68
|
+
* what A39 reads is whether one triangle's sign CHANGED.
|
|
69
|
+
*/
|
|
70
|
+
export function triangleAreas(world: ArrayLike<number>, triangles: ArrayLike<number>): number[] {
|
|
71
|
+
const out: number[] = [];
|
|
72
|
+
for (let t = 0; t + 2 < triangles.length; t += 3) {
|
|
73
|
+
const i0 = triangles[t] * 2;
|
|
74
|
+
const i1 = triangles[t + 1] * 2;
|
|
75
|
+
const i2 = triangles[t + 2] * 2;
|
|
76
|
+
const x0 = world[i0];
|
|
77
|
+
const y0 = world[i0 + 1];
|
|
78
|
+
out.push(0.5 * ((world[i1] - x0) * (world[i2 + 1] - y0) - (world[i2] - x0) * (world[i1 + 1] - y0)));
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The dead band a set of areas is read against.
|
|
85
|
+
*
|
|
86
|
+
* Both bands, and the wider one wins. The relative one is about the SHAPE (a
|
|
87
|
+
* triangle with no area has no winding); the noise one is about the arithmetic (a
|
|
88
|
+
* sign read off float32 rounding is not a measurement). Each is the larger on a
|
|
89
|
+
* different mesh.
|
|
90
|
+
*/
|
|
91
|
+
export function areaBand(plainAreas: readonly number[], ...worlds: ReadonlyArray<ArrayLike<number>>): number {
|
|
92
|
+
const largest = plainAreas.reduce((m, a) => Math.max(m, Math.abs(a)), 0);
|
|
93
|
+
let band = largest * DEFORM_AREA_EPSILON;
|
|
94
|
+
for (const world of worlds) band = Math.max(band, float32AreaNoise(world));
|
|
95
|
+
return band;
|
|
96
|
+
}
|
package/src/deformsurvey.ts
CHANGED
|
@@ -25,72 +25,12 @@
|
|
|
25
25
|
import { CoreInputError, underSkin, type CompiledDocument } from './core/index.ts';
|
|
26
26
|
import { float32Rows, meshWorld, poseDial as corePoseDial, poseJump, recordIdentity, sliderRecordOf, slotDraw, type CoreSurveyPose } from './core/hooks.ts';
|
|
27
27
|
import { modelStructure, type SurveyAnimation, type SurveyDeformTimeline, type SurveyMesh, type SurveySkin, type SurveySlider, type SurveyStructure } from './deformstructure.ts';
|
|
28
|
+
import { areaBand, DEFORM_AREA_EPSILON, float32AreaNoise, triangleAreas } from './areaband.ts';
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* A RELATIVE band, because an absolute one has no scale that means anything on
|
|
34
|
-
* its own — these are pixel² figures on whatever plate the rig was drawn at, and
|
|
35
|
-
* `gallery/flex`'s leaf tops out at 792.6 px² where `spineboy-pro`'s hoverboard
|
|
36
|
-
* reaches 3338.4 px². On those two it comes to 7.9e-4 and 3.3e-3 px².
|
|
37
|
-
*
|
|
38
|
-
* ⚠️ It is **not** what holds the float32 noise off; `float32AreaNoise` is, and
|
|
39
|
-
* the two are combined rather than ranked because on a big mesh the noise bound
|
|
40
|
-
* is the larger of them. This one is the shape band: it keeps a setup triangle
|
|
41
|
-
* that has no area from being read as a reversal of anything, and a triangle the
|
|
42
|
-
* key collapses onto zero from being read as turned over.
|
|
43
|
-
*/
|
|
44
|
-
export const DEFORM_AREA_EPSILON = 1e-6;
|
|
45
|
-
|
|
46
|
-
/** Half an ulp of a float32 mantissa — the relative error of one stored coordinate. */
|
|
47
|
-
const FLOAT32_HALF_ULP = 2 ** -24;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* An upper bound on how much of a triangle's signed area is float32 noise.
|
|
51
|
-
*
|
|
52
|
-
* The world vertices arrive in a `Float32Array`, so each coordinate carries up
|
|
53
|
-
* to `|c|·2⁻²⁴` of error. An area is `½·(Δx₁·Δy₂ − Δx₂·Δy₁)`, and propagating
|
|
54
|
-
* that error through one product gives `Δ·|c|·2⁻²⁴` twice over; four such terms
|
|
55
|
-
* across the two products, halved, bounds the area error by `2·C²·2⁻²⁴` with `C`
|
|
56
|
-
* the largest coordinate magnitude in the mesh (which also bounds every `Δ`).
|
|
57
|
-
* Doubled once more for the subtraction, so the constant is 4.
|
|
58
|
-
*
|
|
59
|
-
* On a mesh whose vertices reach 500 units that is 6e-2 px², i.e. **larger** than
|
|
60
|
-
* the relative band above — which is the whole reason this exists. It is a bound
|
|
61
|
-
* rather than a measurement, and deliberately loose: what has to stay clear of it
|
|
62
|
-
* is a genuine reversal, and the smallest one anywhere in the corpus is
|
|
63
|
-
* `spineboy-pro`'s hoverboard triangle at 8.478 px², more than two orders of
|
|
64
|
-
* magnitude above. Nothing measured lands between the two, so nothing between
|
|
65
|
-
* them is being tuned.
|
|
66
|
-
*/
|
|
67
|
-
export function float32AreaNoise(world: ArrayLike<number>): number {
|
|
68
|
-
let coordinate = 0;
|
|
69
|
-
for (let i = 0; i < world.length; i++) coordinate = Math.max(coordinate, Math.abs(world[i]));
|
|
70
|
-
return 4 * coordinate * coordinate * FLOAT32_HALF_ULP;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Twice-signed area, halved, of every triangle of `triangles` over the
|
|
75
|
-
* interleaved `x, y` world vertices in `world`.
|
|
76
|
-
*
|
|
77
|
-
* The SIGN is the whole point and the magnitude is the tolerance's yardstick, so
|
|
78
|
-
* this returns the signed figure rather than an absolute one. Positive and
|
|
79
|
-
* negative are not "correct" and "wrong" — a mesh may be wound either way, and
|
|
80
|
-
* what A39 reads is whether one triangle's sign CHANGED.
|
|
81
|
-
*/
|
|
82
|
-
export function triangleAreas(world: ArrayLike<number>, triangles: ArrayLike<number>): number[] {
|
|
83
|
-
const out: number[] = [];
|
|
84
|
-
for (let t = 0; t + 2 < triangles.length; t += 3) {
|
|
85
|
-
const i0 = triangles[t] * 2;
|
|
86
|
-
const i1 = triangles[t + 1] * 2;
|
|
87
|
-
const i2 = triangles[t + 2] * 2;
|
|
88
|
-
const x0 = world[i0];
|
|
89
|
-
const y0 = world[i0 + 1];
|
|
90
|
-
out.push(0.5 * ((world[i1] - x0) * (world[i2 + 1] - y0) - (world[i2] - x0) * (world[i1 + 1] - y0)));
|
|
91
|
-
}
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
30
|
+
// The area band lives in `src/areaband.ts` (moved unchanged, issue #1224) so the
|
|
31
|
+
// geometry entry can read it without reaching the compiler; every name this
|
|
32
|
+
// module exported before is exported from here still.
|
|
33
|
+
export { DEFORM_AREA_EPSILON, float32AreaNoise, triangleAreas };
|
|
94
34
|
|
|
95
35
|
/**
|
|
96
36
|
* The two singular values of the linear map that takes one triangle onto the
|
|
@@ -1624,21 +1564,6 @@ function measurePosed(
|
|
|
1624
1564
|
};
|
|
1625
1565
|
}
|
|
1626
1566
|
|
|
1627
|
-
/**
|
|
1628
|
-
* The dead band a set of areas is read against.
|
|
1629
|
-
*
|
|
1630
|
-
* Both bands, and the wider one wins. The relative one is about the SHAPE (a
|
|
1631
|
-
* triangle with no area has no winding); the noise one is about the arithmetic (a
|
|
1632
|
-
* sign read off float32 rounding is not a measurement). Each is the larger on a
|
|
1633
|
-
* different mesh.
|
|
1634
|
-
*/
|
|
1635
|
-
function areaBand(plainAreas: readonly number[], ...worlds: ReadonlyArray<ArrayLike<number>>): number {
|
|
1636
|
-
const largest = plainAreas.reduce((m, a) => Math.max(m, Math.abs(a)), 0);
|
|
1637
|
-
let band = largest * DEFORM_AREA_EPSILON;
|
|
1638
|
-
for (const world of worlds) band = Math.max(band, float32AreaNoise(world));
|
|
1639
|
-
return band;
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
1567
|
// ---------------------------------------------------------------------------
|
|
1643
1568
|
// Between two keys — issue #403
|
|
1644
1569
|
// ---------------------------------------------------------------------------
|
package/src/mesh.ts
CHANGED
|
@@ -44,6 +44,16 @@
|
|
|
44
44
|
*/
|
|
45
45
|
import type { ModelBinding, ModelVertices } from './model.ts';
|
|
46
46
|
|
|
47
|
+
// The mesh-quality measurement and its report (issue #1224) live in their own
|
|
48
|
+
// module and reach a dependant through this file, which is what
|
|
49
|
+
// `spine-rigc/mesh` names. That module imports helpers from this one, so the
|
|
50
|
+
// two form an import cycle: safe only because neither reads the other's
|
|
51
|
+
// bindings while it is being evaluated — every use is inside a function.
|
|
52
|
+
// The reduction (`reduceMesh`, stage B2 of #1224) sits in the same cycle, by
|
|
53
|
+
// the same rule.
|
|
54
|
+
export * from './meshquality.ts';
|
|
55
|
+
export * from './meshreduce.ts';
|
|
56
|
+
|
|
47
57
|
export interface MeshSpecInput {
|
|
48
58
|
/** Polygon in part-local pixels, y down, in manifest order. */
|
|
49
59
|
hull: Array<[number, number]>;
|
|
@@ -139,13 +149,34 @@ export interface RibbonSpecInput {
|
|
|
139
149
|
|
|
140
150
|
export class MeshError extends Error {}
|
|
141
151
|
|
|
152
|
+
/**
|
|
153
|
+
* A refusal of the mesh-quality operations (`src/meshquality.ts`, issue #1224),
|
|
154
|
+
* by code: a `MeshError`, so a dependant that already catches `MeshError` keeps
|
|
155
|
+
* catching it, and `code` for one that wants to know which refusal it was. The
|
|
156
|
+
* codes are listed in docs/MESH_REDUCTION.md beside the section that defines
|
|
157
|
+
* each.
|
|
158
|
+
*
|
|
159
|
+
* ⚠️ Defined here and not in `src/meshquality.ts`, because that module imports
|
|
160
|
+
* this one and this one re-exports it: a class that extends `MeshError` at the
|
|
161
|
+
* top of the importing module would read `MeshError` before this module had
|
|
162
|
+
* run, and every import of `spine-rigc/mesh` would throw.
|
|
163
|
+
*/
|
|
164
|
+
export class MeshReductionError extends MeshError {
|
|
165
|
+
readonly code: string;
|
|
166
|
+
constructor(code: string, message: string) {
|
|
167
|
+
super(`${code}: ${message}`);
|
|
168
|
+
this.code = code;
|
|
169
|
+
this.name = 'MeshReductionError';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
142
173
|
/**
|
|
143
174
|
* The generator's own grid: 6 decimals, never "-0". Its rows, weights and
|
|
144
175
|
* shares are built on it, and what it produces reaches the file through the
|
|
145
176
|
* compiler's `f32` at emission (issue #716), so the emitted text is still each
|
|
146
177
|
* number's float32 name — this is the generator's resolution, not the file's.
|
|
147
178
|
*/
|
|
148
|
-
function r6(n: number): number {
|
|
179
|
+
export function r6(n: number): number {
|
|
149
180
|
const v = Math.round(n * 1e6) / 1e6;
|
|
150
181
|
return v === 0 ? 0 : v;
|
|
151
182
|
}
|
|
@@ -681,7 +712,7 @@ const CRACK_DIRS: ReadonlyArray<readonly [number, number]> = [
|
|
|
681
712
|
];
|
|
682
713
|
|
|
683
714
|
/** Perpendicular distance from `p` to the segment `a`-`b`, clamped to its ends. */
|
|
684
|
-
function distanceToSegment(
|
|
715
|
+
export function distanceToSegment(
|
|
685
716
|
p: readonly [number, number],
|
|
686
717
|
a: readonly [number, number],
|
|
687
718
|
b: readonly [number, number],
|
|
@@ -843,7 +874,7 @@ export function prunePolygon(poly: Array<[number, number]>): Array<[number, numb
|
|
|
843
874
|
}
|
|
844
875
|
|
|
845
876
|
/** Do two segments share a point? Touching counts — an ear needs strict simplicity. */
|
|
846
|
-
function segmentsMeet(
|
|
877
|
+
export function segmentsMeet(
|
|
847
878
|
a: readonly [number, number],
|
|
848
879
|
b: readonly [number, number],
|
|
849
880
|
c: readonly [number, number],
|
|
@@ -979,14 +1010,14 @@ export function earClip(poly: Array<[number, number]>): number[] {
|
|
|
979
1010
|
}
|
|
980
1011
|
|
|
981
1012
|
/** Which pixels of a mask are art, as 1/0 bytes. */
|
|
982
|
-
function artOf(mask: AlphaMask, threshold: number): Uint8Array {
|
|
1013
|
+
export function artOf(mask: AlphaMask, threshold: number): Uint8Array {
|
|
983
1014
|
const out = new Uint8Array(mask.width * mask.height);
|
|
984
1015
|
for (let i = 0; i < out.length; i++) out[i] = mask.alpha[i] >= threshold ? 1 : 0;
|
|
985
1016
|
return out;
|
|
986
1017
|
}
|
|
987
1018
|
|
|
988
1019
|
/** 4-connected island labels, 1-based; 0 is background. */
|
|
989
|
-
function labelIslands(art: Uint8Array, w: number, h: number): { label: Int32Array; sizes: number[] } {
|
|
1020
|
+
export function labelIslands(art: Uint8Array, w: number, h: number): { label: Int32Array; sizes: number[] } {
|
|
990
1021
|
const label = new Int32Array(w * h);
|
|
991
1022
|
const sizes: number[] = [];
|
|
992
1023
|
const stack: number[] = [];
|
|
@@ -1185,7 +1216,7 @@ export function traceAlphaOutline(
|
|
|
1185
1216
|
* The authored-mesh measurement floods 4, as it always has; the two agree on
|
|
1186
1217
|
* every mask the trace accepts.
|
|
1187
1218
|
*/
|
|
1188
|
-
function fillEnclosed(
|
|
1219
|
+
export function fillEnclosed(
|
|
1189
1220
|
inside: Uint8Array,
|
|
1190
1221
|
w: number,
|
|
1191
1222
|
h: number,
|
|
@@ -1242,7 +1273,7 @@ function fillEnclosed(
|
|
|
1242
1273
|
* same convention `src/render.ts` rasterises by. A degenerate triangle — zero
|
|
1243
1274
|
* doubled area — is skipped rather than given an orientation it does not have.
|
|
1244
1275
|
*/
|
|
1245
|
-
function rasteriseTriangles(points: Array<[number, number]>, triangles: number[], w: number, h: number): Uint8Array {
|
|
1276
|
+
export function rasteriseTriangles(points: Array<[number, number]>, triangles: number[], w: number, h: number): Uint8Array {
|
|
1246
1277
|
const covered = new Uint8Array(w * h);
|
|
1247
1278
|
for (let t = 0; t + 2 < triangles.length; t += 3) {
|
|
1248
1279
|
const a = points[triangles[t]];
|
|
@@ -1420,7 +1451,7 @@ export function measureAuthoredMeshFit(
|
|
|
1420
1451
|
* a distance. A grid with no set pixel at all comes back all `INF`; the one caller
|
|
1421
1452
|
* never asks (a mask with no art has no covered-outside pixel to ask about).
|
|
1422
1453
|
*/
|
|
1423
|
-
function squaredDistanceToSet(inside: Uint8Array, w: number, h: number): Float64Array {
|
|
1454
|
+
export function squaredDistanceToSet(inside: Uint8Array, w: number, h: number): Float64Array {
|
|
1424
1455
|
const INF = w * w + h * h + 1;
|
|
1425
1456
|
const dist = new Float64Array(w * h);
|
|
1426
1457
|
for (let i = 0; i < dist.length; i++) dist[i] = inside[i] ? 0 : INF;
|