spine-rigc 2.15.0 → 2.16.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/docs/AUTHORING.md +2 -2
- package/package.json +4 -1
- package/src/mesh.ts +58 -18
package/docs/AUTHORING.md
CHANGED
|
@@ -2617,8 +2617,8 @@ mesh rather than a rim prefix, and `A28_RIBBON_ROWS_SHARE_WEIGHTS` **SKIPs** wit
|
|
|
2617
2617
|
| --- | --- |
|
|
2618
2618
|
| every pixel opaque | `every pixel of the 96x64 part reaches alpha 1, so its silhouette IS the part window and a contour mesh of it is a region attachment with extra vertices` |
|
|
2619
2619
|
| two or more islands | `the art is 2 separate islands and one outline can only enclose the largest (529 of 989 px, 53.49%)` — one outline encloses one region; give each island its own slot |
|
|
2620
|
-
| a **hole** (a donut) | **accepted, and the hole is inside the mesh.** Ear clipping has no bridging step, so the outline is the art's *outer* boundary; those pixels draw nothing (their alpha is still 0) and the extra triangles are the whole cost. `build` and `explain` print the hole in pixels, because `coverage` and `overshoot` are both measured against the FILLED silhouette and neither of them moves when a part gains one |
|
|
2621
|
-
| a **diagonal pinch** — two parts of the art meeting at one pixel corner | `the alpha silhouette pinches to a single point at pixel corner (2,2) … one outline cannot pass through one point twice` |
|
|
2620
|
+
| a **hole** (a donut) | **accepted, and the hole is inside the mesh.** Ear clipping has no bridging step, so the outline is the art's *outer* boundary; those pixels draw nothing (their alpha is still 0) and the extra triangles are the whole cost. A hole is a transparent pixel that no path of transparent pixels — stepping through edges *or corners* — reaches from the part's border: the island is 4-connected, so a transparent pixel that meets the outside only at a corner is outside, not a hole. `build` and `explain` print the hole in pixels, because `coverage` and `overshoot` are both measured against the FILLED silhouette and neither of them moves when a part gains one |
|
|
2621
|
+
| a **diagonal pinch** — two parts of the art meeting at one pixel corner, with the outside on the other diagonal | `the alpha silhouette pinches to a single point at pixel corner (2,2) … one outline cannot pass through one point twice. Move the alpha threshold so the pinch opens (raise it) or closes (lower it), or author the geometry as weights` — the corner is scanned for on the **filled** silhouette, the one the outline is traced around. A corner where two holes meet, or a hole and the art, is inside that silhouette and the outline never reaches it, so it is **not** a pinch: a 4x4 block with (1,1) and (2,2) clear traces as the square, enclosing 2px of hole. A 3x3 ring with its centre and (2,2) clear is refused at (2,2), because the centre meets the outside there |
|
|
2622
2622
|
| a neck narrower than `margin` | `the outline crosses itself: edge 0 meets edge 3 after a margin of 3px was pushed out of a silhouette narrower than that` |
|
|
2623
2623
|
| more outline than `maxVertices` | `the silhouette simplified to 15 vertices at tolerance 1.5, past the 4 this mesh allows` — refused, never silently decimated |
|
|
2624
2624
|
| a `margin` too small for the `tolerance` | `the mesh covers 91.81% of the art (2936 of 3198 px), under the 99.5% a contour mesh guarantees` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.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": {
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"./render": "./src/render.ts",
|
|
14
14
|
"./png": "./src/png.ts",
|
|
15
15
|
"./compile": "./src/compile.ts",
|
|
16
|
+
"./rig": "./src/rig.ts",
|
|
17
|
+
"./mesh": "./src/mesh.ts",
|
|
18
|
+
"./errors": "./src/errors.ts",
|
|
16
19
|
"./cli": "./cli.ts",
|
|
17
20
|
"./package.json": "./package.json",
|
|
18
21
|
"./*.ts": "./*.ts",
|
package/src/mesh.ts
CHANGED
|
@@ -1075,7 +1075,24 @@ export function traceAlphaOutline(
|
|
|
1075
1075
|
const inside = new Uint8Array(w * h);
|
|
1076
1076
|
for (let i = 0; i < inside.length; i++) inside[i] = label[i] === biggest ? 1 : 0;
|
|
1077
1077
|
|
|
1078
|
-
|
|
1078
|
+
// The silhouette the walk traces is the island WITH ITS HOLES FILLED, and it is
|
|
1079
|
+
// built before the pinch scan because the scan has to read the same pixels the
|
|
1080
|
+
// walk does. A corner where two holes meet diagonally is inside the filled
|
|
1081
|
+
// silhouette, and the outer outline never visits it; scanning the island
|
|
1082
|
+
// before the fill refused such a part for a pinch no outline passes through
|
|
1083
|
+
// (issue #1209).
|
|
1084
|
+
//
|
|
1085
|
+
// ⚠️ A hole is background that no 8-connected path of background reaches from
|
|
1086
|
+
// the border, not merely no 4-connected one. The island is 4-connected, so two
|
|
1087
|
+
// background pixels that touch only at a corner are connected through it — the
|
|
1088
|
+
// same duality the walk's clockwise rule keeps. With a 4-connected flood a
|
|
1089
|
+
// transparent pixel that touches the outside only diagonally would read as a
|
|
1090
|
+
// hole, the fill would close the pinch at that corner, and the part would trace
|
|
1091
|
+
// a region the art does not enclose: that is filling a pinch, not a hole. On
|
|
1092
|
+
// every part this function accepts the two floods fill the same pixels — they
|
|
1093
|
+
// can only differ across a corner the scan below refuses.
|
|
1094
|
+
const { filled, holePixels } = fillEnclosed(inside, w, h, 8);
|
|
1095
|
+
const at = (x: number, y: number): number => (x < 0 || y < 0 || x >= w || y >= h ? 0 : filled[y * w + x]);
|
|
1079
1096
|
/** Outgoing cracks at one lattice corner, as direction indices. */
|
|
1080
1097
|
const outgoing = (cx: number, cy: number): number[] => {
|
|
1081
1098
|
const dirs: number[] = [];
|
|
@@ -1087,12 +1104,17 @@ export function traceAlphaOutline(
|
|
|
1087
1104
|
};
|
|
1088
1105
|
|
|
1089
1106
|
// A DIAGONAL PINCH is refused before the walk, not during it. At a corner where
|
|
1090
|
-
// two
|
|
1091
|
-
// cracks leave the same point — so the boundary is not a
|
|
1092
|
-
// any more, and whichever pairing the walk chooses it
|
|
1093
|
-
// point twice or leaves part of the outline
|
|
1094
|
-
// second one produces a perfectly valid mesh of
|
|
1095
|
-
// it costs one pass and names the pixel corner.
|
|
1107
|
+
// two pixels of the filled silhouette meet diagonally with background on the
|
|
1108
|
+
// other diagonal, two cracks leave the same point — so the boundary is not a
|
|
1109
|
+
// set of simple loops any more, and whichever pairing the walk chooses it
|
|
1110
|
+
// either passes through one point twice or leaves part of the outline
|
|
1111
|
+
// untraced. Both are silent: the second one produces a perfectly valid mesh of
|
|
1112
|
+
// the wrong region. Scanning for it costs one pass and names the pixel corner.
|
|
1113
|
+
// Background here is what the fill left, so both pixels on that diagonal are
|
|
1114
|
+
// reached from outside the part: the corner is on the outer outline itself.
|
|
1115
|
+
// The remedy names a direction for each outcome because a threshold moves art
|
|
1116
|
+
// one way only: raising it removes pixels, which can open a pinch and never
|
|
1117
|
+
// close one, and lowering it adds them.
|
|
1096
1118
|
for (let cy = 0; cy <= h; cy++) {
|
|
1097
1119
|
for (let cx = 0; cx <= w; cx++) {
|
|
1098
1120
|
const tl = at(cx - 1, cy - 1);
|
|
@@ -1102,16 +1124,19 @@ export function traceAlphaOutline(
|
|
|
1102
1124
|
if ((tl && br && !tr && !bl) || (tr && bl && !tl && !br)) {
|
|
1103
1125
|
throw new MeshError(
|
|
1104
1126
|
`the alpha silhouette pinches to a single point at pixel corner (${cx},${cy}), where two parts of the art ` +
|
|
1105
|
-
'meet diagonally — one outline cannot pass through one point twice.
|
|
1106
|
-
'pinch
|
|
1127
|
+
'meet diagonally — one outline cannot pass through one point twice. Move the alpha threshold so the ' +
|
|
1128
|
+
'pinch opens (raise it) or closes (lower it), or author the geometry as weights',
|
|
1107
1129
|
);
|
|
1108
1130
|
}
|
|
1109
1131
|
}
|
|
1110
1132
|
}
|
|
1111
1133
|
|
|
1134
|
+
// The first filled pixel in row order is an island pixel: a hole's upper
|
|
1135
|
+
// neighbour is never background the border reaches, so no hole sits on the
|
|
1136
|
+
// silhouette's top row.
|
|
1112
1137
|
let startAt = -1;
|
|
1113
|
-
for (let i = 0; i <
|
|
1114
|
-
if (
|
|
1138
|
+
for (let i = 0; i < filled.length; i++) {
|
|
1139
|
+
if (filled[i]) {
|
|
1115
1140
|
startAt = i;
|
|
1116
1141
|
break;
|
|
1117
1142
|
}
|
|
@@ -1119,10 +1144,9 @@ export function traceAlphaOutline(
|
|
|
1119
1144
|
const sx = startAt % w;
|
|
1120
1145
|
const sy = (startAt - sx) / w;
|
|
1121
1146
|
// With no pinch anywhere, every corner has at most one outgoing crack, so the
|
|
1122
|
-
// boundary is a
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1125
|
-
// makes "holes are filled" a property of the trace rather than a later repair.
|
|
1147
|
+
// boundary is a simple loop and this walk traces it. The holes have no loop
|
|
1148
|
+
// of their own to enter, because the walk reads the filled silhouette: their
|
|
1149
|
+
// pixels are inside the outline and draw nothing, and `holePixels` counts them.
|
|
1126
1150
|
const outline: Array<[number, number]> = [];
|
|
1127
1151
|
let cx = sx;
|
|
1128
1152
|
let cy = sy;
|
|
@@ -1144,7 +1168,6 @@ export function traceAlphaOutline(
|
|
|
1144
1168
|
dir = next;
|
|
1145
1169
|
}
|
|
1146
1170
|
|
|
1147
|
-
const { filled, holePixels } = fillEnclosed(inside, w, h);
|
|
1148
1171
|
return { outline, artPixels, islandPixels: sizes[biggest - 1], islands: sizes.length, holePixels, filled };
|
|
1149
1172
|
}
|
|
1150
1173
|
|
|
@@ -1155,8 +1178,19 @@ export function traceAlphaOutline(
|
|
|
1155
1178
|
* than by a winding rule — which is what makes it answer the same question for a
|
|
1156
1179
|
* mask of one island and a mask of several, and is why the authored-mesh
|
|
1157
1180
|
* measurement can pass it all the art where the trace passes it one island.
|
|
1181
|
+
*
|
|
1182
|
+
* `background` is how the flood steps: 4 through edges, 8 through corners too.
|
|
1183
|
+
* The trace floods 8 — the dual of its 4-connected island, so background that
|
|
1184
|
+
* meets the outside at a corner is outside — and says why where it calls this.
|
|
1185
|
+
* The authored-mesh measurement floods 4, as it always has; the two agree on
|
|
1186
|
+
* every mask the trace accepts.
|
|
1158
1187
|
*/
|
|
1159
|
-
function fillEnclosed(
|
|
1188
|
+
function fillEnclosed(
|
|
1189
|
+
inside: Uint8Array,
|
|
1190
|
+
w: number,
|
|
1191
|
+
h: number,
|
|
1192
|
+
background: 4 | 8,
|
|
1193
|
+
): { filled: Uint8Array; holePixels: number } {
|
|
1160
1194
|
const outsideReach = new Uint8Array(w * h);
|
|
1161
1195
|
const queue: number[] = [];
|
|
1162
1196
|
const seed = (x: number, y: number): void => {
|
|
@@ -1182,6 +1216,12 @@ function fillEnclosed(inside: Uint8Array, w: number, h: number): { filled: Uint8
|
|
|
1182
1216
|
seed(x + 1, y);
|
|
1183
1217
|
seed(x, y - 1);
|
|
1184
1218
|
seed(x, y + 1);
|
|
1219
|
+
if (background === 8) {
|
|
1220
|
+
seed(x - 1, y - 1);
|
|
1221
|
+
seed(x + 1, y - 1);
|
|
1222
|
+
seed(x - 1, y + 1);
|
|
1223
|
+
seed(x + 1, y + 1);
|
|
1224
|
+
}
|
|
1185
1225
|
}
|
|
1186
1226
|
const filled = new Uint8Array(w * h);
|
|
1187
1227
|
let holePixels = 0;
|
|
@@ -1340,7 +1380,7 @@ export function measureAuthoredMeshFit(
|
|
|
1340
1380
|
): MeshFitReport {
|
|
1341
1381
|
const { width: w, height: h } = mask;
|
|
1342
1382
|
const art = artOf(mask, threshold);
|
|
1343
|
-
const { filled } = fillEnclosed(art, w, h);
|
|
1383
|
+
const { filled } = fillEnclosed(art, w, h, 4);
|
|
1344
1384
|
const covered = rasteriseTriangles(points, triangles, w, h);
|
|
1345
1385
|
let artPixels = 0;
|
|
1346
1386
|
let coveredArt = 0;
|