rig-c 2.21.0 → 2.23.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/NOTICE.md CHANGED
@@ -8,8 +8,10 @@ Copyright (c) 2013-2025 Esoteric Software LLC, licensed under the
8
8
  [Spine Runtimes License Agreement](https://esotericsoftware.com/spine-runtimes-license),
9
9
  as a **development dependency**: a clone and CI install it, and every Spine file
10
10
  `rigc build` writes there is read back through it before it is written. **The
11
- published package does not carry it** — an install of `spine-rigc` has no Spine
12
- runtime in it unless one is installed beside it, and then the same `rigc` uses it.
11
+ published package does not carry it** — an install of `rig-c` (or of
12
+ `spine-rigc`, the same files under the name the package first shipped as) has no
13
+ Spine runtime in it unless one is installed beside it, and then the same `rigc`
14
+ uses it.
13
15
 
14
16
  Its terms, as the licence states them: integration of the Spine Runtimes into
15
17
  software is permitted **under the terms and conditions of Section 2 of the
package/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <a href="https://www.npmjs.com/package/spine-rigc"><img src="https://img.shields.io/npm/v/spine-rigc.svg?style=flat-square&color=FF6B4A" alt="npm version" /></a>
7
- <a href="https://www.npmjs.com/package/spine-rigc"><img src="https://img.shields.io/npm/dm/spine-rigc.svg?style=flat-square&color=A855F7" alt="npm downloads" /></a>
6
+ <a href="https://www.npmjs.com/package/rig-c"><img src="https://img.shields.io/npm/v/rig-c.svg?style=flat-square&color=FF6B4A" alt="npm version" /></a>
7
+ <a href="https://www.npmjs.com/package/rig-c"><img src="https://img.shields.io/npm/dm/rig-c.svg?style=flat-square&color=A855F7" alt="npm downloads" /></a>
8
8
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-38BDF8.svg?style=flat-square" alt="license" /></a>
9
9
  </p>
10
10
 
@@ -13,7 +13,8 @@ a rig compiler for Spine: a rig spec and a motion spec in, Spine 4.3 skeleton da
13
13
  gated by a list of named assertions before a byte is written — rigc's own validator in
14
14
  the published package, held to a `spine-core` round trip's verdicts in this
15
15
  repository's CI. Built so AI agents can author rigs and check their own work; it ships
16
- as an agent skill.
16
+ as an agent skill. On npm it is `rig-c` — the same package as `spine-rigc`, the name it
17
+ first shipped under, which is published beside it with the same files at every version.
17
18
 
18
19
  ## What you get
19
20
 
@@ -143,18 +144,22 @@ runners and on Linux at the declared minimum, Bun 1.2.0 (the `installs` and
143
144
  repository material, not in the npm package); a platform
144
145
  whose leg is red there is not one the package is known to run on.
145
146
 
146
- **The npm package is `spine-rigc`; the command it installs is `rigc`.** npm
147
+ **The npm package is `rig-c`; the command it installs is `rigc`.** npm
147
148
  refuses the name `rigc` as too similar to packages that already exist, so the
148
- project, this repository and the executable keep their name and only the
149
- registry entry is spelled out.
149
+ project, this repository and the executable keep their name and the registry
150
+ entry carries one hyphen. **`spine-rigc` is the same package**: it is the name
151
+ every version up to 2.20.4 shipped under, and it stays published as an alias —
152
+ every version from 2.20.4 on is on both names, with the same files — so a
153
+ project that depends on `spine-rigc` keeps receiving every release. A new
154
+ project installs `rig-c`.
150
155
 
151
156
  ```bash
152
- bunx spine-rigc --help # run it without installing
153
- bun add -g spine-rigc # or install the command
154
- bun add -d spine-rigc # or pin it in a project
157
+ bunx rig-c --help # run it without installing
158
+ bun add -g rig-c # or install the command
159
+ bun add -d rig-c # or pin it in a project
155
160
  ```
156
161
 
157
- `npx spine-rigc` works too, as long as Bun is on `PATH` — the executable is a
162
+ `npx rig-c` works too, as long as Bun is on `PATH` — the executable is a
158
163
  Bun script, and npm only writes the shim that calls it.
159
164
 
160
165
  Installed, the command is `rigc`. The examples below spell it `bun cli.ts`
@@ -194,7 +199,7 @@ plugin marketplace:
194
199
  /plugin install rigc@rigc
195
200
  ```
196
201
 
197
- With the package already installed, `claude --plugin-dir node_modules/spine-rigc`
202
+ With the package already installed, `claude --plugin-dir node_modules/rig-c`
198
203
  loads the same skills without a marketplace. The plugin carries no version of its
199
204
  own — `/plugin update` follows `main` commit by commit, and the only version on
200
205
  disk stays the one in `package.json`.
@@ -206,12 +211,12 @@ Codex, Gemini CLI and Antigravity read skills from one directory in the workspac
206
211
  `node_modules`. With the package installed, one command puts every skill there:
207
212
 
208
213
  ```shell
209
- bun add -d spine-rigc
210
- bun rigc skills install # relative links: .agents/skills/rigc -> ../../node_modules/spine-rigc/skills/rigc
214
+ bun add -d rig-c
215
+ bun rigc skills install # relative links: .agents/skills/rigc -> ../../node_modules/rig-c/skills/rigc
211
216
  bun rigc skills install --copy # the folders themselves, for a host that does not follow a link
212
217
  ```
213
218
 
214
- Run it through the project's own install, as above: `bunx spine-rigc skills install`
219
+ Run it through the project's own install, as above: `bunx rig-c skills install`
215
220
  in a project that has the package was measured running the registry's copy instead
216
221
  of the project's. A link reaches every upgrade of the package with no second run,
217
222
  and a second run has nothing to do; an entry already there that this command did not
@@ -242,11 +247,11 @@ reference export, so nothing you read in a quickstart is an answer to anything
242
247
  **1. Install the command.**
243
248
 
244
249
  ```bash
245
- bun add -g spine-rigc # installs `rigc`
250
+ bun add -g rig-c # installs `rigc`
246
251
  ```
247
252
 
248
253
  Or skip the install and prefix every command below with `bunx `, e.g.
249
- `bunx spine-rigc build …`.
254
+ `bunx rig-c build …`.
250
255
 
251
256
  **2. Make a directory and three plates.** rigc measures PNGs rather than trusting
252
257
  a number you typed (R5), so the art has to exist. These three are solid colours a
@@ -450,7 +455,7 @@ that does not belong to it. See
450
455
  field by field, the emission rules, every named failure mapped to the file that
451
456
  has to change, and §8–§9 for reproducing a shot you were given as pictures. It
452
457
  ships inside the npm package too, at
453
- `node_modules/spine-rigc/docs/AUTHORING.md`.
458
+ `node_modules/rig-c/docs/AUTHORING.md`.
454
459
  - `rigc explain --rig buoy.rig.json --motion buoy.motion.json --out spine` prints
455
460
  the compiled rig as a table — every bone with its resolved parent, the slots in
456
461
  draw order, every timeline key by key — and writes nothing. It is what to reach
@@ -744,7 +749,7 @@ letting `A17` blame the editor for the harness's own doing.
744
749
 
745
750
  | Document | For |
746
751
  | --- | --- |
747
- | 📘 **[docs/AUTHORING.md](docs/AUTHORING.md)** | **the format guide, and the one to read before writing a spec.** Both input files field by field with a complete minimal example each, every field with its Spine meaning, the rules that decide what is emitted, the build → read the report → fix → repeat loop, the map from every named failure to the file that has to change, and the features rigc refuses by name so you do not spend a loop discovering them. It travels **inside the npm package**, at `node_modules/spine-rigc/docs/AUTHORING.md` |
752
+ | 📘 **[docs/AUTHORING.md](docs/AUTHORING.md)** | **the format guide, and the one to read before writing a spec.** Both input files field by field with a complete minimal example each, every field with its Spine meaning, the rules that decide what is emitted, the build → read the report → fix → repeat loop, the map from every named failure to the file that has to change, and the features rigc refuses by name so you do not spend a loop discovering them. It travels **inside the npm package**, at `node_modules/rig-c/docs/AUTHORING.md` |
748
753
  | 🦴 **[docs/RIGGING.md](docs/RIGGING.md)** | **authoring the hierarchy.** Where a bone goes and why the art is pushed out on an offset, why a pivot in the wrong place looks like a search failure and what identifies one, moving a pivot and the child row that gets forgotten, gauges, siblings-not-a-chain, what a chain can reach and how many links it needs, why a local key is not a world key, duplicate art at mirrored pivots, and constraints as structure. Every section is a stumble the run records hold more than once, ranked by how often. Ships in the package too |
749
754
  | 🎞️ **[docs/MOTION.md](docs/MOTION.md)** | **the key-pose recipe.** How to get two poses, what a pair of poses does and does not fix, the in-betweening rules and where each comes from, and how to spread candidates so a ballot informs. Ships in the package too |
750
755
  | 🙂 **[docs/FACE.md](docs/FACE.md)** | **authoring a face.** A blink, a gaze and a 2.5D head turn on plain Spine data: the one line of yaw arithmetic every number in a turn comes from, depth as the parameter you are actually authoring, where to put a grid's columns and the angle at which any grid folds, what foreshortens and what does not, channel allocation before the first key, and the three cliffs with their angles. Also the deform audit gap, demonstrated — a folded mesh gates green — and the differential check that works today |
package/docs/AUTHORING.md CHANGED
@@ -275,14 +275,14 @@ for `vote` (default `ballot.html`).
275
275
  `skills install` is the one command that is not about a rig: it puts the agent
276
276
  skills the package ships where an agent host looks for them. Run it through the
277
277
  project's own install — `bun rigc skills install` — so the links point into that
278
- project's `node_modules/spine-rigc/skills/`. A second run has nothing to do and
278
+ project's `node_modules/rig-c/skills/`. A second run has nothing to do and
279
279
  exits 0. An entry already at `<dir>/<name>` that is not a link to the same folder
280
280
  (or, under `--copy`, not the same bytes) is refused, exit 1, and **nothing is
281
281
  written** — every such entry is named with what is there and what was required:
282
282
 
283
283
  ```text
284
284
  rigc skills install: <n> of the <m> skill(s) cannot be installed into <dir>, and nothing was written:
285
- <dir>/rigc-motion is a plain file; a symlink to ../../node_modules/spine-rigc/skills/rigc-motion was required
285
+ <dir>/rigc-motion is a plain file; a symlink to ../../node_modules/rig-c/skills/rigc-motion was required
286
286
  Remove the entries named above, or pass --dir to install somewhere else.
287
287
  ```
288
288
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rig-c",
3
- "version": "2.21.0",
3
+ "version": "2.23.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": {
@@ -2,7 +2,7 @@
2
2
  name: rigc
3
3
  description: Author, build and validate Spine 4.3 skeleton data (skeleton.json plus its .atlas) from loose part PNGs with rigc, the rig compiler that verifies its own output with named assertions before writing it. Use for any request to make a Spine rig or Spine animation from PNG parts, or where the source is a Live2D, Unity or video model whose pictures you can render, to run or read rigc build, validate, render, preview, check or vote, or to write or fix a *.rig.json or *.motion.json spec; it says which shipped guide to open for the need at hand. Not for Live2D conversion, cutting an illustration into parts, or real-time face tracking.
4
4
  license: MIT
5
- compatibility: Requires Bun 1.2 or later. The tool is the npm package spine-rigc (bunx spine-rigc, or bun add -d spine-rigc); the command it installs is rigc.
5
+ compatibility: Requires Bun 1.2 or later. The tool is the npm package rig-c (bunx rig-c, or bun add -d rig-c); the command it installs is rigc.
6
6
  ---
7
7
 
8
8
  # rigc — a rig compiler for agents
@@ -35,8 +35,8 @@ whole interface, and this skill only says which of them to open.
35
35
  ## Install
36
36
 
37
37
  ```shell
38
- bunx spine-rigc --help # run it without installing
39
- bun add -d spine-rigc # or pin it in the project; the command is `rigc`
38
+ bunx rig-c --help # run it without installing
39
+ bun add -d rig-c # or pin it in the project; the command is `rigc`
40
40
  bun rigc skills install # then link these skills into .agents/skills
41
41
  ```
42
42
 
@@ -126,8 +126,8 @@ spec files field by field, the emission rules, the loop, and the failure map. Th
126
126
  | a **skeleton.json somebody else authored** — read it, repair it, extend it | [INGEST.md](https://github.com/firejune/rigc/blob/main/docs/INGEST.md) | `rigc-ingest` |
127
127
  | you are the **person operating** the agent rather than the agent | [PROMPTING.md](https://github.com/firejune/rigc/blob/main/docs/PROMPTING.md) | — |
128
128
 
129
- Every guide linked here is in the installed package at `node_modules/spine-rigc/docs/`,
129
+ Every guide linked here is in the installed package at `node_modules/rig-c/docs/`,
130
130
  which is the copy that matches the rigc you run; the links go to the repository's
131
131
  `main`. Inside the Claude Code plugin the same files are at `${CLAUDE_PLUGIN_ROOT}/docs/`.
132
132
  Formats, the CLI reference and the licence chain:
133
- [README.md](https://github.com/firejune/rigc/blob/main/README.md), installed at `node_modules/spine-rigc/README.md`.
133
+ [README.md](https://github.com/firejune/rigc/blob/main/README.md), installed at `node_modules/rig-c/README.md`.
@@ -2,7 +2,7 @@
2
2
  name: rigc-face
3
3
  description: Author a face on plain Spine data with rigc — a blink, a gaze shift, a breathing portrait and a head turn a few degrees off axis, built from deform timelines and per-part parallax. Use when the request is a talking or living portrait, a standing character, an expression or a head turn, such as "rig this face", "make the portrait blink and look around" or "turn the head". Not for Live2D file conversion, cutting a face illustration into parts, or VTuber-style real-time face tracking.
4
4
  license: MIT
5
- compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
5
+ compatibility: Requires Bun 1.2 or later and the npm package rig-c.
6
6
  ---
7
7
 
8
8
  # Face — a turn, a gaze and a blink
@@ -52,7 +52,7 @@ differential audit and the three limits it does not lift.
52
52
  rather than this closed form, and [INGEST.md](https://github.com/firejune/rigc/blob/main/docs/INGEST.md) if the head
53
53
  arrived as a compiled skeleton.
54
54
 
55
- Every guide linked here is in the installed package at `node_modules/spine-rigc/docs/`,
55
+ Every guide linked here is in the installed package at `node_modules/rig-c/docs/`,
56
56
  which is the copy that matches the rigc you run; the links go to the repository's
57
57
  `main`. Inside the Claude Code plugin the same files are at `${CLAUDE_PLUGIN_ROOT}/docs/`.
58
58
 
@@ -2,7 +2,7 @@
2
2
  name: rigc-ingest
3
3
  description: Work with a Spine skeleton.json somebody else authored — exported from the Spine editor or another tool — using rigc. Read and validate it, understand a complaint rigc raised about it, decompile it into rigc specs with `rigc ingest`, normalise, re-pivot or rename it, and extend it with an animation it does not have. Use when the input is an existing skeleton.json with its .atlas and page images rather than loose part PNGs. Not for Live2D file conversion or runtime tracking.
4
4
  license: MIT
5
- compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
5
+ compatibility: Requires Bun 1.2 or later and the npm package rig-c.
6
6
  ---
7
7
 
8
8
  # Ingest — a skeleton you did not author
@@ -70,7 +70,7 @@ thing to reach for; transcription by hand is what you fall back on for a constru
70
70
  3. Then [RIGGING.md](https://github.com/firejune/rigc/blob/main/docs/RIGGING.md) for why the re-pivot edit has the shape
71
71
  it has, and [MOTION.md](https://github.com/firejune/rigc/blob/main/docs/MOTION.md) for the animation you are adding.
72
72
 
73
- Every guide linked here is in the installed package at `node_modules/spine-rigc/docs/`,
73
+ Every guide linked here is in the installed package at `node_modules/rig-c/docs/`,
74
74
  which is the copy that matches the rigc you run; the links go to the repository's
75
75
  `main`. Inside the Claude Code plugin the same files are at `${CLAUDE_PLUGIN_ROOT}/docs/`.
76
76
 
@@ -2,7 +2,7 @@
2
2
  name: rigc-motion
3
3
  description: Author a Spine animation with rigc from key poses — an idle, a loop, a move from one picture to another — with timing and spacing, ease in and out, anticipation, arcs, overlap, follow-through, squash and stretch, and candidate variants a person can choose between. Use when the request is a movement on an existing or planned rig, in the animator's words too: "animate this rig", "make it breathe", "go from pose A to pose B", "make it feel heavier", "the cape should follow through". Not for Live2D, separating an image into parts, or real-time tracking.
4
4
  license: MIT
5
- compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
5
+ compatibility: Requires Bun 1.2 or later and the npm package rig-c.
6
6
  ---
7
7
 
8
8
  # Motion — what goes between two poses
@@ -43,7 +43,7 @@ thing that judges a movement is a person's eye through `rigc vote` — MOTION §
43
43
  head turn; [INGEST.md](https://github.com/firejune/rigc/blob/main/docs/INGEST.md) if the rig arrived as a compiled
44
44
  skeleton rather than loose parts.
45
45
 
46
- Every guide linked here is in the installed package at `node_modules/spine-rigc/docs/`,
46
+ Every guide linked here is in the installed package at `node_modules/rig-c/docs/`,
47
47
  which is the copy that matches the rigc you run; the links go to the repository's
48
48
  `main`. Inside the Claude Code plugin the same files are at `${CLAUDE_PLUGIN_ROOT}/docs/`.
49
49
 
@@ -2,7 +2,7 @@
2
2
  name: rigc-rigging
3
3
  description: Decide a Spine rig's hierarchy with rigc — how many bones, where each pivot sits, what hangs off what, offsets, chains and what a chain can reach, siblings versus chains, constraints as structure — and which of those decisions the reference frames can check. Use when the request is a skeleton from loose part PNGs, such as "rig these parts", "make a Spine skeleton" or "where do the joints go", before any motion is authored. Not for Live2D, cutting an illustration into parts, or VTuber-style tracking.
4
4
  license: MIT
5
- compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
5
+ compatibility: Requires Bun 1.2 or later and the npm package rig-c.
6
6
  ---
7
7
 
8
8
  # Rigging — the hierarchy itself
@@ -41,7 +41,7 @@ are named in RIGGING §11 — neither as a pass bar.
41
41
  [INGEST.md](https://github.com/firejune/rigc/blob/main/docs/INGEST.md) if the skeleton was handed to you already
42
42
  compiled.
43
43
 
44
- Every guide linked here is in the installed package at `node_modules/spine-rigc/docs/`,
44
+ Every guide linked here is in the installed package at `node_modules/rig-c/docs/`,
45
45
  which is the copy that matches the rigc you run; the links go to the repository's
46
46
  `main`. Inside the Claude Code plugin the same files are at `${CLAUDE_PLUGIN_ROOT}/docs/`.
47
47
 
package/src/areaband.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * — and `stretchSingularValues` after them (issue #1230), for the motion
9
9
  * comparison (`src/meshcompare.ts`), which reads a triangle's stretch the way
10
10
  * the deform survey does and must not reach the compiler either.
11
- * They moved because a module the `spine-rigc/mesh` entry reaches has to read
11
+ * They moved because a module the `rig-c/mesh` entry reaches has to read
12
12
  * the same band, and `src/deformsurvey.ts` reaches the compiler and the core
13
13
  * (`src/deformstructure.ts`, `src/core/`) — a geometry entry that loaded the
14
14
  * compiler to read three numbers would also close an import cycle through
package/src/cli/shared.ts CHANGED
@@ -905,7 +905,7 @@ export const DEFAULT_LEDGER = 'votes.jsonl';
905
905
  // skills install — put the shipped skills where an agent host looks (issue #831)
906
906
  // ---------------------------------------------------------------------------
907
907
  //
908
- // After `bun add -d spine-rigc` the skills sit at `node_modules/spine-rigc/skills/`,
908
+ // After `bun add -d rig-c` the skills sit at `node_modules/rig-c/skills/`,
909
909
  // which no host reads. Codex, Gemini CLI and Antigravity all read
910
910
  // `<workspace>/.agents/skills/<name>/`, so this links every `skills/<name>/` the
911
911
  // package ships into one directory — `.agents/skills` under the working
package/src/mesh.ts CHANGED
@@ -46,7 +46,7 @@ import type { ModelBinding, ModelVertices } from './model.ts';
46
46
 
47
47
  // The mesh-quality measurement and its report (issue #1224) live in their own
48
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
49
+ // `rig-c/mesh` names. That module imports helpers from this one, so the
50
50
  // two form an import cycle: safe only because neither reads the other's
51
51
  // bindings while it is being evaluated — every use is inside a function.
52
52
  // The reduction (`reduceMesh`, stage B2 of #1224) sits in the same cycle, by
@@ -163,7 +163,7 @@ export class MeshError extends Error {}
163
163
  * ⚠️ Defined here and not in `src/meshquality.ts`, because that module imports
164
164
  * this one and this one re-exports it: a class that extends `MeshError` at the
165
165
  * top of the importing module would read `MeshError` before this module had
166
- * run, and every import of `spine-rigc/mesh` would throw.
166
+ * run, and every import of `rig-c/mesh` would throw.
167
167
  */
168
168
  export class MeshReductionError extends MeshError {
169
169
  readonly code: string;
@@ -1253,8 +1253,9 @@ export function traceAlphaOutline(
1253
1253
  * `background` is how the flood steps: 4 through edges, 8 through corners too.
1254
1254
  * The trace floods 8 — the dual of its 4-connected island, so background that
1255
1255
  * meets the outside at a corner is outside — and says why where it calls this.
1256
- * The authored-mesh measurement floods 4, as it always has; the two agree on
1257
- * every mask the trace accepts.
1256
+ * The authored-mesh measurement floods 4 by default, as it always has, and 8
1257
+ * when its caller asks (issue #1262); the two agree on every mask the trace
1258
+ * accepts.
1258
1259
  */
1259
1260
  export function fillEnclosed(
1260
1261
  inside: Uint8Array,
@@ -1479,16 +1480,57 @@ export interface MeshFitReport {
1479
1480
  * is never refused, so every figure needs a number — hence the exact distance
1480
1481
  * transform below rather than a neighbourhood search that would have to stop
1481
1482
  * somewhere.
1483
+ *
1484
+ * ## Which background is enclosed — `connectivity` (issue #1262)
1485
+ *
1486
+ * The background is flooded 4-connected by default, as it always has been, so
1487
+ * every call that passes four arguments reads exactly what it read before.
1488
+ * `8` floods through corners too — the fill `measureMeshQuality`'s
1489
+ * `MQ_OVERSHOOT` and `MQ_HOLES` rows are taken against (P12), by the same
1490
+ * `fillEnclosed` call — so a background pocket joined to the outside only
1491
+ * diagonally is read as outside, and a caller gating with this figure reads
1492
+ * the number the rows read. The two fills agree on every mask without such a
1493
+ * pocket. Any other value is refused by name: a silent fallback to one of the
1494
+ * two would answer a question the caller did not ask.
1482
1495
  */
1483
1496
  export function measureAuthoredMeshFit(
1484
1497
  mask: AlphaMask,
1485
1498
  threshold: number,
1486
1499
  points: Array<[number, number]>,
1487
1500
  triangles: number[],
1501
+ connectivity: 4 | 8 = 4,
1488
1502
  ): MeshFitReport {
1503
+ return authoredMeshFitOf(mask, threshold, points, triangles, connectivity, null).report;
1504
+ }
1505
+
1506
+ /**
1507
+ * A fault planted on purpose in `authoredMeshFitOf`, for the mesh-quality
1508
+ * suite's negative control: `'fill-4-under-8'` takes the connectivity it is
1509
+ * passed and floods 4 whatever it was. `measureAuthoredMeshFit` plants none.
1510
+ */
1511
+ export type AuthoredFitPlant = 'fill-4-under-8';
1512
+
1513
+ /**
1514
+ * `measureAuthoredMeshFit`'s measurement, with the pixel its overshoot is
1515
+ * read at — the first covered pixel outside the filled silhouette at the
1516
+ * largest distance, by the tie rule `MQ_OVERSHOOT` names its pixel with, or
1517
+ * -1 when no covered pixel lies outside — so a control can name where two
1518
+ * readings part.
1519
+ */
1520
+ export function authoredMeshFitOf(
1521
+ mask: AlphaMask,
1522
+ threshold: number,
1523
+ points: Array<[number, number]>,
1524
+ triangles: number[],
1525
+ connectivity: 4 | 8,
1526
+ plant: AuthoredFitPlant | null,
1527
+ ): { report: MeshFitReport; overshootAt: number } {
1528
+ if (connectivity !== 4 && connectivity !== 8) {
1529
+ throw new MeshError(`measureAuthoredMeshFit: connectivity is ${JSON.stringify(connectivity)}; required 4 (the default, the legacy fill) or 8 (the fill MQ_OVERSHOOT reads, P12)`);
1530
+ }
1489
1531
  const { width: w, height: h } = mask;
1490
1532
  const art = artOf(mask, threshold);
1491
- const { filled } = fillEnclosed(art, w, h, 4);
1533
+ const { filled } = fillEnclosed(art, w, h, plant === 'fill-4-under-8' ? 4 : connectivity);
1492
1534
  const covered = rasteriseTriangles(points, triangles, w, h);
1493
1535
  let artPixels = 0;
1494
1536
  let coveredArt = 0;
@@ -1499,15 +1541,22 @@ export function measureAuthoredMeshFit(
1499
1541
  }
1500
1542
  const squared = squaredDistanceToSet(filled, w, h);
1501
1543
  let worst = 0;
1544
+ let overshootAt = -1;
1502
1545
  for (let i = 0; i < covered.length; i++) {
1503
1546
  if (!covered[i] || filled[i]) continue;
1504
- if (squared[i] > worst) worst = squared[i];
1547
+ if (overshootAt === -1 || squared[i] > worst) {
1548
+ overshootAt = i;
1549
+ worst = squared[i];
1550
+ }
1505
1551
  }
1506
1552
  return {
1507
- artPixels,
1508
- coveredArt,
1509
- coverage: artPixels === 0 ? 0 : coveredArt / artPixels,
1510
- overshoot: r6(Math.sqrt(worst)),
1553
+ report: {
1554
+ artPixels,
1555
+ coveredArt,
1556
+ coverage: artPixels === 0 ? 0 : coveredArt / artPixels,
1557
+ overshoot: r6(Math.sqrt(worst)),
1558
+ },
1559
+ overshootAt,
1511
1560
  };
1512
1561
  }
1513
1562
 
@@ -32,7 +32,8 @@
32
32
  * taken at (`MeasureRow.art.threshold`); a measurement claims nothing at any
33
33
  * other.
34
34
  * - **Change the legacy fit.** `measureAuthoredMeshFit` (`src/mesh.ts`) keeps
35
- * its 4-connected fill for every existing caller. The rows here fill with the
35
+ * its 4-connected fill for every existing caller (its `connectivity: 8`,
36
+ * issue #1262, is the fill the rows read). The rows here fill with the
36
37
  * tracer's 8-connected background flood over all art (P12), and where the two
37
38
  * fills differ — a diagonal pinch — both labelled results are rows.
38
39
  * - **Pose, or link the runtime.** Pure: no clock, no randomness, no network,
@@ -694,8 +695,21 @@ function validateInput(input: MeshMeasureInput): void {
694
695
 
695
696
  type Pt = readonly [number, number];
696
697
 
697
- /** Is `p` inside or on the closed polygon? On the boundary within `ON_BOUNDARY` counts as inside. */
698
- function inClosedPolygon(p: Pt, poly: readonly Pt[]): boolean {
698
+ /**
699
+ * Is `p` inside or on the closed polygon? On the boundary within `ON_BOUNDARY` counts as inside.
700
+ *
701
+ * The rule, exactly, because `closedPolygonCentres` reproduces it bit for bit:
702
+ * `p` is in when its `distanceToSegment` to some side `poly[i]`–`poly[i+1]`
703
+ * is at most `ON_BOUNDARY` (1e-9 px) — a point on an edge or at a vertex is in,
704
+ * whatever the ray says; otherwise by **even-odd** parity of a ray towards +x,
705
+ * where the side from `poly[j]` to `poly[i]` (`j` the one before `i`) crosses
706
+ * when exactly one of its ends has a y strictly greater than `p`'s — `yi > py`
707
+ * differs from `yj > py`, so the half-open rule counts a vertex at `p`'s height
708
+ * once and a horizontal side never — at
709
+ * `xc = ((xj − xi)·(py − yi)) / (yj − yi) + xi`, and the crossing counts when
710
+ * `px < xc` strictly.
711
+ */
712
+ export function inClosedPolygon(p: Pt, poly: readonly Pt[]): boolean {
699
713
  const n = poly.length;
700
714
  for (let i = 0; i < n; i++) if (distanceToSegment(p, poly[i], poly[(i + 1) % n]) <= ON_BOUNDARY) return true;
701
715
  let inside = false;
@@ -707,6 +721,112 @@ function inClosedPolygon(p: Pt, poly: readonly Pt[]): boolean {
707
721
  return inside;
708
722
  }
709
723
 
724
+ /**
725
+ * A fault planted on purpose in `closedPolygonCentres`, for the mesh-quality
726
+ * suite's negative control: `'crossing-half-pixel'` moves every ray crossing
727
+ * half a pixel towards +x. `regionRows` plants none.
728
+ */
729
+ export type ScanlinePlant = 'crossing-half-pixel';
730
+
731
+ /**
732
+ * Which pixel centres of a `w`×`h` grid lie in or on a closed polygon given
733
+ * in drawing px — `inClosedPolygon` of every centre `((x + 0.5) / scale,
734
+ * (y + 0.5) / scale)`, 1 where it is true, by scanline (issue #1263).
735
+ *
736
+ * The same decision, not a cleaner one: each row's crossings are the sides
737
+ * `inClosedPolygon` counts for that row's centre height, by its own
738
+ * half-open test, at the `xc` it computes with the same expression in the same
739
+ * order, so they are the same doubles; sorted, the number of them a centre
740
+ * lies strictly left of is the number its ray counts, and its parity is the
741
+ * answer. The boundary band is then added side by side, each centre near a
742
+ * side tested with `distanceToSegment` against `ON_BOUNDARY` — the predicate's
743
+ * own test — so a centre on an edge or at a vertex is in exactly when the
744
+ * predicate says so. The rows and columns visited around a side are widened by
745
+ * a pixel past where it can reach, and only the exact tests decide.
746
+ *
747
+ * O(rows × sides + pixels) where testing every centre against every side is
748
+ * O(pixels × sides).
749
+ */
750
+ export function closedPolygonCentres(poly: readonly Pt[], w: number, h: number, scale: number, plant: ScanlinePlant | null = null): Uint8Array {
751
+ const marks = new Uint8Array(w * h);
752
+ const n = poly.length;
753
+ if (n === 0 || w <= 0 || h <= 0) return marks;
754
+ const cx = (x: number): number => (x + 0.5) / scale;
755
+ const cy = (y: number): number => (y + 0.5) / scale;
756
+ const shift = plant === 'crossing-half-pixel' ? 0.5 / scale : 0;
757
+ /** The grid row or column of a drawing coordinate, rounded one way and widened by `slack`; NaN visits nothing. */
758
+ const cellOf = (v: number, round: (u: number) => number, slack: number, size: number): number => {
759
+ const c = round(v * scale - 0.5) + slack;
760
+ if (Number.isNaN(c)) return slack < 0 ? size : -1;
761
+ return c;
762
+ };
763
+
764
+ // Even-odd: per row, the crossings of a ray towards +x from the row's centre height.
765
+ const crossings: Array<number[] | undefined> = new Array<number[] | undefined>(h);
766
+ for (let i = 0, j = n - 1; i < n; j = i++) {
767
+ const [xi, yi] = poly[i];
768
+ const [xj, yj] = poly[j];
769
+ const y0 = Math.max(0, cellOf(Math.min(yi, yj), Math.floor, -1, h));
770
+ const y1 = Math.min(h - 1, cellOf(Math.max(yi, yj), Math.ceil, 1, h));
771
+ for (let y = y0; y <= y1; y++) {
772
+ const py = cy(y);
773
+ if (yi > py !== yj > py) {
774
+ let row = crossings[y];
775
+ if (row === undefined) {
776
+ row = [];
777
+ crossings[y] = row;
778
+ }
779
+ row.push(((xj - xi) * (py - yi)) / (yj - yi) + xi + shift);
780
+ }
781
+ }
782
+ }
783
+ for (let y = 0; y < h; y++) {
784
+ const row = crossings[y];
785
+ if (row === undefined) continue;
786
+ row.sort((a, b) => a - b);
787
+ const x0 = Math.max(0, cellOf(row[0], Math.floor, -1, w));
788
+ const x1 = Math.min(w - 1, cellOf(row[row.length - 1], Math.ceil, 1, w));
789
+ let left = 0;
790
+ for (let x = x0; x <= x1; x++) {
791
+ const px = cx(x);
792
+ while (left < row.length && !(px < row[left])) left++;
793
+ if ((row.length - left) & 1) marks[y * w + x] = 1;
794
+ }
795
+ }
796
+
797
+ // On the boundary: within ON_BOUNDARY of a side. Per side, the rows its ends span and, per row, the columns
798
+ // the side passes within a pixel of that row's centre height, each widened by a pixel; the exact test decides.
799
+ const unit = 1 / scale;
800
+ for (let i = 0; i < n; i++) {
801
+ const a = poly[i];
802
+ const b = poly[(i + 1) % n];
803
+ const y0 = Math.max(0, cellOf(Math.min(a[1], b[1]), Math.floor, -1, h));
804
+ const y1 = Math.min(h - 1, cellOf(Math.max(a[1], b[1]), Math.ceil, 1, h));
805
+ const dy = b[1] - a[1];
806
+ for (let y = y0; y <= y1; y++) {
807
+ let lo = Math.min(a[0], b[0]);
808
+ let hi = Math.max(a[0], b[0]);
809
+ if (dy !== 0) {
810
+ const py = cy(y);
811
+ const t0 = Math.max(0, Math.min(1, (py - unit - a[1]) / dy));
812
+ const t1 = Math.max(0, Math.min(1, (py + unit - a[1]) / dy));
813
+ const xa = a[0] + (b[0] - a[0]) * t0;
814
+ const xb = a[0] + (b[0] - a[0]) * t1;
815
+ lo = Math.min(xa, xb);
816
+ hi = Math.max(xa, xb);
817
+ }
818
+ const x0 = Math.max(0, cellOf(lo, Math.floor, -1, w));
819
+ const x1 = Math.min(w - 1, cellOf(hi, Math.ceil, 1, w));
820
+ for (let x = x0; x <= x1; x++) {
821
+ const k = y * w + x;
822
+ if (marks[k]) continue;
823
+ if (distanceToSegment([cx(x), cy(y)], a, b) <= ON_BOUNDARY) marks[k] = 1;
824
+ }
825
+ }
826
+ }
827
+ return marks;
828
+ }
829
+
710
830
  /** Does the segment `a`–`b` meet the closed polygon — an endpoint inside or on it, or any crossing or touch? */
711
831
  function segmentMeetsPolygon(a: Pt, b: Pt, poly: readonly Pt[]): boolean {
712
832
  if (inClosedPolygon(a, poly) || inClosedPolygon(b, poly)) return true;
@@ -1154,7 +1274,7 @@ export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
1154
1274
  * taken from another art are refused (`REDUCE_ART_RASTERS_MISMATCH`), never
1155
1275
  * read.
1156
1276
  *
1157
- * Internal: it is on `spine-rigc/mesh` only because that entry re-exports this
1277
+ * Internal: it is on `rig-c/mesh` only because that entry re-exports this
1158
1278
  * module with `export *`, and a symbol that is merely exported is not promised
1159
1279
  * (RELEASING.md, *The import surface*).
1160
1280
  */
@@ -1673,8 +1793,10 @@ function regionRows(input: MeshMeasureInput, outline: MeshOutline, hullPolygon:
1673
1793
  // would keep, in the same order.
1674
1794
  const centreOf = (i: number): Pt => [((i % mask.width) + 0.5) / scale, (Math.floor(i / mask.width) + 0.5) / scale];
1675
1795
  const inRegion = (): Int32Array => {
1796
+ // `inClosedPolygon` of every art pixel centre, by scanline (`closedPolygonCentres`, issue #1263).
1797
+ const inside = closedPolygonCentres(poly, mask.width, mask.height, scale);
1676
1798
  const kept: number[] = [];
1677
- for (let i = 0; i < artBits.length; i++) if (artBits[i] && inClosedPolygon(centreOf(i), poly)) kept.push(i);
1799
+ for (let i = 0; i < artBits.length; i++) if (artBits[i] && inside[i]) kept.push(i);
1678
1800
  return Int32Array.from(kept);
1679
1801
  };
1680
1802
  const regionPixels = steps === null ? inRegion() : steps.regionPixels(poly, inRegion);
@@ -31,7 +31,7 @@
31
31
  * the value the rasters were taken at and the value the input requires
32
32
  * (`checkArtRasters`, `src/meshquality.ts`).
33
33
  *
34
- * Not re-exported by `src/mesh.ts`: nothing here is on `spine-rigc/mesh`.
34
+ * Not re-exported by `src/mesh.ts`: nothing here is on `rig-c/mesh`.
35
35
  */
36
36
  import { artOf, distancePassesOf, fillEnclosed, labelIslands, MeshError, pixelCentreInTriangle, prunePolygon, squaredDistanceToSet, traceAlphaOutline, triangleRasterBox } from './mesh.ts';
37
37
  import type { ArtInput } from './meshquality.ts';
package/src/meshreduce.ts CHANGED
@@ -1211,7 +1211,7 @@ export function reduceMesh(input: MeshReductionInput): MeshReductionResult {
1211
1211
  * which is the path the carried one is held equal to. Step rasters made over
1212
1212
  * another rasters object are refused by the same code.
1213
1213
  *
1214
- * Internal: it is on `spine-rigc/mesh` only because that entry re-exports this
1214
+ * Internal: it is on `rig-c/mesh` only because that entry re-exports this
1215
1215
  * module with `export *`, and a symbol that is merely exported is not promised
1216
1216
  * (RELEASING.md, *The import surface*).
1217
1217
  */