spine-rigc 2.19.0 → 2.20.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 +2 -1
- package/src/areaband.ts +64 -1
- package/src/cli/shared.ts +4 -31
- package/src/deformsurvey.ts +6 -64
- package/src/mesh.ts +52 -12
- package/src/meshcompare.ts +1188 -0
- package/src/meshquality.ts +251 -20
- package/src/meshreduce.ts +92 -6
- package/src/package_meta.ts +48 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.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": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"./rig": "./src/rig.ts",
|
|
17
17
|
"./mesh": "./src/mesh.ts",
|
|
18
18
|
"./errors": "./src/errors.ts",
|
|
19
|
+
"./meshcompare": "./src/meshcompare.ts",
|
|
19
20
|
"./cli": "./cli.ts",
|
|
20
21
|
"./package.json": "./package.json",
|
|
21
22
|
"./*.ts": "./*.ts",
|
package/src/areaband.ts
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* degeneracy by (`src/meshquality.ts`).
|
|
5
5
|
*
|
|
6
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
|
|
7
|
+
* and re-exports every name it exported before, so no caller changed an import
|
|
8
|
+
* — and `stretchSingularValues` after them (issue #1230), for the motion
|
|
9
|
+
* comparison (`src/meshcompare.ts`), which reads a triangle's stretch the way
|
|
10
|
+
* the deform survey does and must not reach the compiler either.
|
|
8
11
|
* They moved because a module the `spine-rigc/mesh` entry reaches has to read
|
|
9
12
|
* the same band, and `src/deformsurvey.ts` reaches the compiler and the core
|
|
10
13
|
* (`src/deformstructure.ts`, `src/core/`) — a geometry entry that loaded the
|
|
@@ -94,3 +97,63 @@ export function areaBand(plainAreas: readonly number[], ...worlds: ReadonlyArray
|
|
|
94
97
|
for (const world of worlds) band = Math.max(band, float32AreaNoise(world));
|
|
95
98
|
return band;
|
|
96
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The two singular values of the linear map that takes one triangle onto the
|
|
103
|
+
* other — the largest and smallest factor by which it scales a direction.
|
|
104
|
+
*
|
|
105
|
+
* ## Why this is the texture's stretch
|
|
106
|
+
*
|
|
107
|
+
* A mesh's uvs are fixed to the attachment and a deform never touches them, so
|
|
108
|
+
* the texture is mapped affinely onto the *plain* triangle and the same texels
|
|
109
|
+
* end up on the *deformed* one. The change in that mapping is exactly `J = D·P⁻¹`
|
|
110
|
+
* with `P` and `D` the two triangles' edge pairs, and its singular values are the
|
|
111
|
+
* worst stretch and the worst squash the drawing takes there. A σ of 1.4 means
|
|
112
|
+
* every texel in that direction is drawn 1.4 px wide; 0.6 means the art is
|
|
113
|
+
* crushed to 60%.
|
|
114
|
+
*
|
|
115
|
+
* `σ₁·σ₂ = |det J|` is the signed-area ratio's magnitude, which is why the two
|
|
116
|
+
* quantities in the report cannot disagree — and `DR01` is the control that says
|
|
117
|
+
* so on a case whose ratio the closed form predicts.
|
|
118
|
+
*
|
|
119
|
+
* Returns `null` for a plain triangle with no area: `P` is singular, there is no
|
|
120
|
+
* map, and inventing one would be the report's own version of the false green
|
|
121
|
+
* this file exists to avoid. Those triangles are counted as `degenerate`.
|
|
122
|
+
*/
|
|
123
|
+
export function stretchSingularValues(
|
|
124
|
+
plain: ArrayLike<number>,
|
|
125
|
+
deformed: ArrayLike<number>,
|
|
126
|
+
triangles: ArrayLike<number>,
|
|
127
|
+
t: number,
|
|
128
|
+
): { max: number; min: number } | null {
|
|
129
|
+
const i0 = triangles[t * 3] * 2;
|
|
130
|
+
const i1 = triangles[t * 3 + 1] * 2;
|
|
131
|
+
const i2 = triangles[t * 3 + 2] * 2;
|
|
132
|
+
const ux = plain[i1] - plain[i0];
|
|
133
|
+
const uy = plain[i1 + 1] - plain[i0 + 1];
|
|
134
|
+
const vx = plain[i2] - plain[i0];
|
|
135
|
+
const vy = plain[i2 + 1] - plain[i0 + 1];
|
|
136
|
+
const det = ux * vy - vx * uy;
|
|
137
|
+
if (det === 0) return null;
|
|
138
|
+
const px = deformed[i1] - deformed[i0];
|
|
139
|
+
const py = deformed[i1 + 1] - deformed[i0 + 1];
|
|
140
|
+
const qx = deformed[i2] - deformed[i0];
|
|
141
|
+
const qy = deformed[i2 + 1] - deformed[i0 + 1];
|
|
142
|
+
// J = D·P⁻¹, written out — P⁻¹ = (1/det)·[[vy, −vx], [−uy, ux]].
|
|
143
|
+
const a = (px * vy - qx * uy) / det;
|
|
144
|
+
const b = (-px * vx + qx * ux) / det;
|
|
145
|
+
const c = (py * vy - qy * uy) / det;
|
|
146
|
+
const d = (-py * vx + qy * ux) / det;
|
|
147
|
+
// σ₁² + σ₂² = ‖J‖²_F and σ₁·σ₂ = |det J|, which is two equations for the two
|
|
148
|
+
// values and needs no eigen decomposition. The discriminant is non-negative in
|
|
149
|
+
// exact arithmetic (it is `(σ₁² − σ₂²)²`); the clamp is for rounding on a map
|
|
150
|
+
// that is very nearly a rotation.
|
|
151
|
+
const frobenius = a * a + b * b + c * c + d * d;
|
|
152
|
+
const determinant = a * d - b * c;
|
|
153
|
+
const discriminant = Math.max(0, frobenius * frobenius - 4 * determinant * determinant);
|
|
154
|
+
const root = Math.sqrt(discriminant);
|
|
155
|
+
return {
|
|
156
|
+
max: Math.sqrt(Math.max(0, (frobenius + root) / 2)),
|
|
157
|
+
min: Math.sqrt(Math.max(0, (frobenius - root) / 2)),
|
|
158
|
+
};
|
|
159
|
+
}
|
package/src/cli/shared.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { depthStepLevels, type FoldLimit, type TurnCeiling } from '../depth.ts';
|
|
|
39
39
|
import { parseJsonWithPosition } from '../json-position.ts';
|
|
40
40
|
import { RUNG_IDS } from '../ladder.ts';
|
|
41
41
|
import { MODEL_DOCUMENT_FILE, MODEL_DOCUMENT_SPEC, modelDocument, spineFileSha256 } from '../model.ts';
|
|
42
|
+
import { PACKAGE_ROOT, readPackageMeta, readVersion } from '../package_meta.ts';
|
|
42
43
|
import { DEFAULT_MAX_RESIDUAL, DEFAULT_SCALE_MAX, DEFAULT_SCALE_MIN } from '../pose.ts';
|
|
43
44
|
import {
|
|
44
45
|
CandidateAtlasError,
|
|
@@ -98,37 +99,9 @@ export class ExplainError extends Error {}
|
|
|
98
99
|
// and for naming a remedy `bench` can only give from a repo checkout.
|
|
99
100
|
// ---------------------------------------------------------------------------
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let packageMeta: PackageMeta | null | undefined;
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* The directory `cli.ts` sits in: `package.json`, `skills/` and, in a
|
|
110
|
-
* checkout, `examples/` and `scripts/` are beside it, in the repository and
|
|
111
|
-
* once installed. Two levels above this file (`src/cli/`), which is where the
|
|
112
|
-
* commands that read those live since issue #1052 — every path they built off
|
|
113
|
-
* `cli.ts`'s own directory is built off this one, and comes out the same.
|
|
114
|
-
*/
|
|
115
|
-
export const PACKAGE_ROOT = join(import.meta.dir, '..', '..');
|
|
116
|
-
|
|
117
|
-
/** `package.json` sits next to `cli.ts` both in the repo and once installed (`PACKAGE_ROOT`). */
|
|
118
|
-
export function readPackageMeta(): PackageMeta | null {
|
|
119
|
-
if (packageMeta === undefined) {
|
|
120
|
-
try {
|
|
121
|
-
packageMeta = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')) as PackageMeta;
|
|
122
|
-
} catch {
|
|
123
|
-
packageMeta = null;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return packageMeta;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function readVersion(): string {
|
|
130
|
-
return readPackageMeta()?.version ?? 'unknown';
|
|
131
|
-
}
|
|
102
|
+
// The reader moved to `../package_meta.ts` (issue #1230), so a library module can read
|
|
103
|
+
// the version without loading the CLI; every name it had here is re-exported unchanged.
|
|
104
|
+
export { PACKAGE_ROOT, readPackageMeta, readVersion };
|
|
132
105
|
|
|
133
106
|
// ---------------------------------------------------------------------------
|
|
134
107
|
// argument parsing
|
package/src/deformsurvey.ts
CHANGED
|
@@ -25,72 +25,14 @@
|
|
|
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
|
+
import { areaBand, DEFORM_AREA_EPSILON, float32AreaNoise, stretchSingularValues, triangleAreas } from './areaband.ts';
|
|
29
29
|
|
|
30
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
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* The two singular values of the linear map that takes one triangle onto the
|
|
37
|
-
* other — the largest and smallest factor by which it scales a direction.
|
|
38
|
-
*
|
|
39
|
-
* ## Why this is the texture's stretch
|
|
40
|
-
*
|
|
41
|
-
* A mesh's uvs are fixed to the attachment and a deform never touches them, so
|
|
42
|
-
* the texture is mapped affinely onto the *plain* triangle and the same texels
|
|
43
|
-
* end up on the *deformed* one. The change in that mapping is exactly `J = D·P⁻¹`
|
|
44
|
-
* with `P` and `D` the two triangles' edge pairs, and its singular values are the
|
|
45
|
-
* worst stretch and the worst squash the drawing takes there. A σ of 1.4 means
|
|
46
|
-
* every texel in that direction is drawn 1.4 px wide; 0.6 means the art is
|
|
47
|
-
* crushed to 60%.
|
|
48
|
-
*
|
|
49
|
-
* `σ₁·σ₂ = |det J|` is the signed-area ratio's magnitude, which is why the two
|
|
50
|
-
* quantities in the report cannot disagree — and `DR01` is the control that says
|
|
51
|
-
* so on a case whose ratio the closed form predicts.
|
|
52
|
-
*
|
|
53
|
-
* Returns `null` for a plain triangle with no area: `P` is singular, there is no
|
|
54
|
-
* map, and inventing one would be the report's own version of the false green
|
|
55
|
-
* this file exists to avoid. Those triangles are counted as `degenerate`.
|
|
56
|
-
*/
|
|
57
|
-
export function stretchSingularValues(
|
|
58
|
-
plain: ArrayLike<number>,
|
|
59
|
-
deformed: ArrayLike<number>,
|
|
60
|
-
triangles: ArrayLike<number>,
|
|
61
|
-
t: number,
|
|
62
|
-
): { max: number; min: number } | null {
|
|
63
|
-
const i0 = triangles[t * 3] * 2;
|
|
64
|
-
const i1 = triangles[t * 3 + 1] * 2;
|
|
65
|
-
const i2 = triangles[t * 3 + 2] * 2;
|
|
66
|
-
const ux = plain[i1] - plain[i0];
|
|
67
|
-
const uy = plain[i1 + 1] - plain[i0 + 1];
|
|
68
|
-
const vx = plain[i2] - plain[i0];
|
|
69
|
-
const vy = plain[i2 + 1] - plain[i0 + 1];
|
|
70
|
-
const det = ux * vy - vx * uy;
|
|
71
|
-
if (det === 0) return null;
|
|
72
|
-
const px = deformed[i1] - deformed[i0];
|
|
73
|
-
const py = deformed[i1 + 1] - deformed[i0 + 1];
|
|
74
|
-
const qx = deformed[i2] - deformed[i0];
|
|
75
|
-
const qy = deformed[i2 + 1] - deformed[i0 + 1];
|
|
76
|
-
// J = D·P⁻¹, written out — P⁻¹ = (1/det)·[[vy, −vx], [−uy, ux]].
|
|
77
|
-
const a = (px * vy - qx * uy) / det;
|
|
78
|
-
const b = (-px * vx + qx * ux) / det;
|
|
79
|
-
const c = (py * vy - qy * uy) / det;
|
|
80
|
-
const d = (-py * vx + qy * ux) / det;
|
|
81
|
-
// σ₁² + σ₂² = ‖J‖²_F and σ₁·σ₂ = |det J|, which is two equations for the two
|
|
82
|
-
// values and needs no eigen decomposition. The discriminant is non-negative in
|
|
83
|
-
// exact arithmetic (it is `(σ₁² − σ₂²)²`); the clamp is for rounding on a map
|
|
84
|
-
// that is very nearly a rotation.
|
|
85
|
-
const frobenius = a * a + b * b + c * c + d * d;
|
|
86
|
-
const determinant = a * d - b * c;
|
|
87
|
-
const discriminant = Math.max(0, frobenius * frobenius - 4 * determinant * determinant);
|
|
88
|
-
const root = Math.sqrt(discriminant);
|
|
89
|
-
return {
|
|
90
|
-
max: Math.sqrt(Math.max(0, (frobenius + root) / 2)),
|
|
91
|
-
min: Math.sqrt(Math.max(0, (frobenius - root) / 2)),
|
|
92
|
-
};
|
|
93
|
-
}
|
|
31
|
+
// geometry entry can read it without reaching the compiler, and
|
|
32
|
+
// `stretchSingularValues` joined it (moved unchanged, issue #1230) so the motion
|
|
33
|
+
// comparison can too; every name this module exported before is exported from
|
|
34
|
+
// here still.
|
|
35
|
+
export { DEFORM_AREA_EPSILON, float32AreaNoise, stretchSingularValues, triangleAreas };
|
|
94
36
|
|
|
95
37
|
/** A quantity's worst triangle on one key, and which triangle it was. */
|
|
96
38
|
export interface DeformExtreme {
|
package/src/mesh.ts
CHANGED
|
@@ -118,7 +118,11 @@ export interface MeshGeometry {
|
|
|
118
118
|
points: Array<[number, number]>;
|
|
119
119
|
/** Normalised region UVs, v measured from the top edge. */
|
|
120
120
|
uvs: number[];
|
|
121
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Triangle indices, counter-clockwise in Spine world (y up) — every builder
|
|
123
|
+
* here, measured on spine-core's posed world by `CT15` in `selftest.ts`
|
|
124
|
+
* (issue #1236: `contour` and `ring` wound the other way until then).
|
|
125
|
+
*/
|
|
122
126
|
triangles: number[];
|
|
123
127
|
/** Per-vertex weights, parallel to `points`. */
|
|
124
128
|
weights: MeshVertexWeight[][];
|
|
@@ -221,6 +225,33 @@ export function signedArea(poly: Array<[number, number]>): number {
|
|
|
221
225
|
return a / 2;
|
|
222
226
|
}
|
|
223
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Turn a triangle list built ALONG `poly` — every triple carrying the
|
|
230
|
+
* polygon's own winding, as an ear-clip of it or a ring strip around it does —
|
|
231
|
+
* counter-clockwise in Spine world, the winding `MeshGeometry.triangles`
|
|
232
|
+
* promises. `poly` is in part-local pixels, y down.
|
|
233
|
+
*
|
|
234
|
+
* Counter-clockwise in Spine world (y up) is a NEGATIVE shoelace area in y-down
|
|
235
|
+
* pixels: the flip changes the sign of the area and not the direction the loop
|
|
236
|
+
* turns as drawn. So a polygon whose y-down area is positive — clockwise on
|
|
237
|
+
* screen, and therefore clockwise in Spine world — has each triangle's last two
|
|
238
|
+
* corners swapped, the way `buildSegmentsLattice` does per triangle; one with a
|
|
239
|
+
* negative area is already right and is returned as it came. The triangle SET
|
|
240
|
+
* is unchanged either way: only the order of two indices inside a triple moves.
|
|
241
|
+
*
|
|
242
|
+
* The decision is the polygon's, not each triangle's, so a near-collinear ear
|
|
243
|
+
* whose own sign is rounding noise is turned with its neighbours rather than
|
|
244
|
+
* read on its own (issue #1236 — `contour` and `ring` emitted every triangle
|
|
245
|
+
* clockwise in Spine world before this existed, under comments that derived the
|
|
246
|
+
* opposite from the y flip).
|
|
247
|
+
*/
|
|
248
|
+
export function windCounterClockwiseInSpineWorld(poly: Array<[number, number]>, triangles: readonly number[]): number[] {
|
|
249
|
+
const out = triangles.slice();
|
|
250
|
+
if (signedArea(poly) <= 0) return out;
|
|
251
|
+
for (let t = 0; t + 2 < out.length; t += 3) [out[t + 1], out[t + 2]] = [out[t + 2], out[t + 1]];
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
224
255
|
/**
|
|
225
256
|
* Is every hull edge visible from `center`? If not, scaling the polygon toward
|
|
226
257
|
* the centre can fold the inner ring through the rim and the triangles cross —
|
|
@@ -379,23 +410,25 @@ export function buildRingMesh(input: MeshSpecInput): MeshGeometry {
|
|
|
379
410
|
const uvs: number[] = [];
|
|
380
411
|
for (const [x, y] of points) uvs.push(r6(x / w), r6(y / h));
|
|
381
412
|
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
|
|
413
|
+
// Each triple below runs along the hull's own list order, so it carries the
|
|
414
|
+
// hull's winding; `windCounterClockwiseInSpineWorld` then turns the list to
|
|
415
|
+
// the winding every generator here emits (issue #1236).
|
|
416
|
+
const along: number[] = [];
|
|
385
417
|
const hub = 3 * n;
|
|
386
418
|
const strip = (outerBase: number, innerBase: number) => {
|
|
387
419
|
for (let i = 0; i < n; i++) {
|
|
388
420
|
const j = (i + 1) % n;
|
|
389
|
-
|
|
390
|
-
|
|
421
|
+
along.push(outerBase + i, outerBase + j, innerBase + j);
|
|
422
|
+
along.push(outerBase + i, innerBase + j, innerBase + i);
|
|
391
423
|
}
|
|
392
424
|
};
|
|
393
425
|
strip(0, n); // window edge -> seam
|
|
394
426
|
strip(n, 2 * n); // seam -> aperture ring
|
|
395
427
|
for (let i = 0; i < n; i++) {
|
|
396
428
|
const j = (i + 1) % n;
|
|
397
|
-
|
|
429
|
+
along.push(2 * n + i, 2 * n + j, hub);
|
|
398
430
|
}
|
|
431
|
+
const triangles = windCounterClockwiseInSpineWorld(hull, along);
|
|
399
432
|
|
|
400
433
|
return { kind: 'ring', points, uvs, triangles, weights, hullVertices: n };
|
|
401
434
|
}
|
|
@@ -940,9 +973,16 @@ function pointInTriangle(
|
|
|
940
973
|
*
|
|
941
974
|
* Repeatedly find a vertex whose two edges make a convex turn in the polygon's
|
|
942
975
|
* own winding and whose triangle holds no other vertex, emit it as a triangle,
|
|
943
|
-
* and remove it. The emitted triples carry the polygon's own winding
|
|
944
|
-
*
|
|
945
|
-
*
|
|
976
|
+
* and remove it. The emitted triples carry the polygon's own winding — the
|
|
977
|
+
* order its list runs in — and nothing else decides it: clipping the same
|
|
978
|
+
* outline mirrored picks the same ears in the same order and writes the same
|
|
979
|
+
* triples. ⚠️ A y flip does NOT turn that winding over. Flipping y changes the
|
|
980
|
+
* sign of a signed area, but the y-up frame is drawn with y up, so the loop
|
|
981
|
+
* turns the same way on screen as it does in Spine world: an outline that is
|
|
982
|
+
* clockwise on screen (positive `signedArea` in y-down pixels) yields
|
|
983
|
+
* triangles that are clockwise in Spine world. A caller that emits them turns
|
|
984
|
+
* them with `windCounterClockwiseInSpineWorld` (issue #1236: this paragraph
|
|
985
|
+
* said the opposite, and `contour` emitted clockwise for that reason).
|
|
946
986
|
*
|
|
947
987
|
* ## What it refuses, and why by name
|
|
948
988
|
*
|
|
@@ -1592,7 +1632,7 @@ export function buildContourMesh(input: ContourSpecInput): MeshGeometry {
|
|
|
1592
1632
|
'has a neck too thin to mesh',
|
|
1593
1633
|
);
|
|
1594
1634
|
}
|
|
1595
|
-
const triangles = earClip(points);
|
|
1635
|
+
const triangles = windCounterClockwiseInSpineWorld(points, earClip(points));
|
|
1596
1636
|
|
|
1597
1637
|
// The bound on the grid the fit is measured on: its last term is one cell of
|
|
1598
1638
|
// that grid, which is why it is derived from the texel figures rather than
|
|
@@ -1905,7 +1945,7 @@ export function meshEdges(vertexCount: number, triangles: readonly number[], hul
|
|
|
1905
1945
|
// bottom-left), each cell two triangles with the diagonal alternating by
|
|
1906
1946
|
// `(i + j) % 2`, so the lattice has no preferred shear. Emitted
|
|
1907
1947
|
// counter-clockwise in Spine world, the winding every generator here
|
|
1908
|
-
// writes (GR02 holds the grid to it).
|
|
1948
|
+
// writes (GR02 holds the grid to it, CT15 every generator).
|
|
1909
1949
|
// 4. **Outline first.** The boundary is walked from the edges used by exactly
|
|
1910
1950
|
// one triangle, in first-seen order, and every interior vertex follows in
|
|
1911
1951
|
// index order — the arrangement `checkHullOrder` requires.
|