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 +4 -2
- package/README.md +23 -18
- package/docs/AUTHORING.md +2 -2
- package/package.json +1 -1
- package/skills/rigc/SKILL.md +5 -5
- package/skills/rigc-face/SKILL.md +2 -2
- package/skills/rigc-ingest/SKILL.md +2 -2
- package/skills/rigc-motion/SKILL.md +2 -2
- package/skills/rigc-rigging/SKILL.md +2 -2
- package/src/areaband.ts +1 -1
- package/src/cli/shared.ts +1 -1
- package/src/mesh.ts +59 -10
- package/src/meshquality.ts +127 -5
- package/src/meshrasters.ts +1 -1
- package/src/meshreduce.ts +1 -1
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 `
|
|
12
|
-
|
|
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/
|
|
7
|
-
<a href="https://www.npmjs.com/package/
|
|
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 `
|
|
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
|
|
149
|
-
|
|
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
|
|
153
|
-
bun add -g
|
|
154
|
-
bun add -d
|
|
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
|
|
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/
|
|
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
|
|
210
|
-
bun rigc skills install # relative links: .agents/skills/rigc -> ../../node_modules/
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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.
|
|
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": {
|
package/skills/rigc/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
39
|
-
bun add -d
|
|
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/
|
|
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/
|
|
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
|
|
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/
|
|
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
|
|
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/
|
|
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
|
|
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/
|
|
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
|
|
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/
|
|
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 `
|
|
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
|
|
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
|
-
// `
|
|
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 `
|
|
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
|
|
1257
|
-
* every mask the trace
|
|
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)
|
|
1547
|
+
if (overshootAt === -1 || squared[i] > worst) {
|
|
1548
|
+
overshootAt = i;
|
|
1549
|
+
worst = squared[i];
|
|
1550
|
+
}
|
|
1505
1551
|
}
|
|
1506
1552
|
return {
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
|
package/src/meshquality.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
698
|
-
|
|
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 `
|
|
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] &&
|
|
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);
|
package/src/meshrasters.ts
CHANGED
|
@@ -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 `
|
|
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 `
|
|
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
|
*/
|