<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Joon Kyoung</title>
    <description>The latest articles on DEV Community by Joon Kyoung (@firejune).</description>
    <link>https://dev.to/firejune</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4202294%2Ff383f123-7129-41dc-9e88-5dfe8812cdde.png</url>
      <title>DEV Community: Joon Kyoung</title>
      <link>https://dev.to/firejune</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/firejune"/>
    <language>en</language>
    <item>
      <title>I built a 2D rig compiler for AI agents, and the validator's messages are the whole UI</title>
      <dc:creator>Joon Kyoung</dc:creator>
      <pubDate>Fri, 23 Oct 2026 10:12:15 +0000</pubDate>
      <link>https://dev.to/firejune/i-built-a-2d-rig-compiler-for-ai-agents-and-the-validators-messages-are-the-whole-ui-4am9</link>
      <guid>https://dev.to/firejune/i-built-a-2d-rig-compiler-for-ai-agents-and-the-validators-messages-are-the-whole-ui-4am9</guid>
      <description>&lt;p&gt;When I had an AI agent rig a 2D character, the only feedback it acted on was whether the file parsed, and a parse is not a look. That one observation is the whole design of rig-c, and this is the story of what follows from it: a compiler that writes nothing until its checks pass, a player that poses characters with &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags and CSS &lt;code&gt;matrix()&lt;/code&gt;, and a public exam where fresh agent sessions try the tools cold. The toolchain's code is MIT, at &lt;a href="https://github.com/rig-c" rel="noopener noreferrer"&gt;https://github.com/rig-c&lt;/a&gt; (the example paintings carry their own licences, noted below), with a demo you can open at &lt;a href="https://rig-c.org/examples/" rel="noopener noreferrer"&gt;https://rig-c.org/examples/&lt;/a&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: parsers that accept broken rigs in silence
&lt;/h2&gt;

&lt;p&gt;A 2D character rig is a skeleton laid over cut-up artwork: bones, slots that hold the part images, meshes with bone weights, constraints, and animations as keyed timelines. Spine's JSON format is the common currency, and it is what rig-c emits.&lt;/p&gt;

&lt;p&gt;The trouble with letting an agent author that format is not that it is hard; it is that the failure modes are quiet. From the project's own list of what a standard parser lets through unremarked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a 4.2-shaped constraint that simply vanishes in 4.3,&lt;/li&gt;
&lt;li&gt;a mismatched &lt;code&gt;size:&lt;/code&gt; that collapses every UV,&lt;/li&gt;
&lt;li&gt;a four-number curve array that yields &lt;code&gt;NaN&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;a mesh whose vertex count equals its UV count, which silently loses its bone weights.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A human in the editor sees the limb detach and fixes it. An agent with only a parser for feedback reports success on a broken rig and is sincere about it. So the first design decision was to convert that silence into a named failure, and to treat the failure text as the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design: five rules
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The messages are the UI.&lt;/strong&gt; Every refusal names the object, the value found and the value required. An agent reads one line and knows which field in which file to change. This is the compiler's own example, after renaming a part image and re-running &lt;code&gt;rigc validate&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAIL  A17_ATLAS_PAGE_FILES_EXIST: page "../images/hull.png" is not on disk at …/images/hull.png
rigc: 1 assertion(s) failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;invalid mesh&lt;/code&gt; is not a message. &lt;code&gt;A20_MESH_WEIGHTS_COHERENT: mesh "x" vertex 12 weights sum to 0.9000&lt;/code&gt; is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Nothing is invented.&lt;/strong&gt; The compiler never guesses a default from the art, never re-measures a plate, never fills in a "reasonable" fallback. If a number is missing, that is a &lt;code&gt;CompileError&lt;/code&gt; naming the field. A compiler that fills gaps produces rigs nobody can reason about, and the build manifest stops being a record of what was measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Emit only after green, and green is structural.&lt;/strong&gt; &lt;code&gt;rigc build&lt;/code&gt; compiles, validates, and writes to disk only if every named assertion of its profile passes: 36 under the default &lt;code&gt;spine&lt;/code&gt; profile (is this valid Spine 4.3 that any runtime plays correctly?), all 50 under the opt-in &lt;code&gt;rig-play&lt;/code&gt; profile. There is no &lt;code&gt;--no-validate&lt;/code&gt;, no &lt;code&gt;--emit-anyway&lt;/code&gt;, no environment escape. That is written into the repository's rules as a structural invariant, not a default. And green judges structure only: in the project's own words, a rig with its head off its torso passes the gate, because those are the offsets the spec asked for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. What was not checked is printed too.&lt;/strong&gt; Every gate report names the profile that judged it and lists the rules that profile left out. The parts tool's &lt;code&gt;check&lt;/code&gt; names each measured bar that said SKIP and why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. A person looks.&lt;/strong&gt; &lt;code&gt;rigc render&lt;/code&gt; and &lt;code&gt;rigc inspect&lt;/code&gt; draw what was built; &lt;code&gt;@rig-c/review&lt;/code&gt; puts it beside the source footage and exports feedback bound to a frame, bone or slot. The checks keep broken data off the disk; a human decides whether it looks right. The output imports into the Spine editor on purpose. The intended hand-off is: an AI drafts, a human refines in the editor. rig-c is not an editor replacement and makes no claim against one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Holding the validator to Spine's own parser
&lt;/h2&gt;

&lt;p&gt;A validator you wrote yourself is a claim, not a fact. The repository holds rig-c's validator to a round trip through &lt;code&gt;@esotericsoftware/spine-core&lt;/code&gt;: in CI, on the public example recipes and every selftest, and on a private corpus measured before each release, every build is parsed by Spine's own runtime and the verdicts are compared. Of the 50 assertions, 49 run on both suppliers; the official parse itself (&lt;code&gt;A00_ROUNDTRIP_PARSE&lt;/code&gt;) runs only where the runtime is installed and is reported as SKIP elsewhere.&lt;/p&gt;

&lt;p&gt;The published package links no Spine runtime: spine-core is a development dependency. That is also what makes the licensing legible. rig-c's code is MIT. Which Spine terms apply depends on what plays the output: &lt;code&gt;@rig-c/play&lt;/code&gt; and the engine adapters link nothing from Esoteric Software, so none apply there; an official Spine Runtime or the Spine editor bring their own terms. The licensing page spells it out in one table: &lt;a href="https://rig-c.org/docs/licensing/" rel="noopener noreferrer"&gt;https://rig-c.org/docs/licensing/&lt;/a&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  The toolchain, end to end, on a CPU
&lt;/h2&gt;

&lt;p&gt;rig-c makes no images. The painting and its layer split come from image models you run yourself, on your GPU or a free web demo. rig-c starts from those files, ships no model weights, and runs every one of its own steps on the CPU.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@rig-c/parts&lt;/code&gt;&lt;/strong&gt; takes one character painting and the layers that See-through (an open-source layer-decomposition model) cut it into, merges them into rig-space parts, then drafts bones, weighted meshes, physics chains and a looping idle. Its README's demo character came out as 23 parts and a 3.3-second breathing loop from one command, &lt;code&gt;rig-parts build --loop --lead-in 3&lt;/code&gt;, with one hand correction recorded in the config beside the value it replaced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@rig-c/pose&lt;/code&gt;&lt;/strong&gt; cleans a body-18 pose sequence (from video, a dataset, or hand-placed points) and retargets it onto the rig as bone keys, gated before anything is written. There is no flag to skip the clean step; a sequence without the clean record is refused by name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rig-c&lt;/code&gt;&lt;/strong&gt; compiles and gates it, as above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@rig-c/review&lt;/code&gt;&lt;/strong&gt; is the human inspection workspace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@rig-c/play&lt;/code&gt;&lt;/strong&gt; plays the compiled model in a page with no Spine runtime, and adapters bring it into PixiJS 8, Phaser 4.2+ and Three.js 0.186+.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1mlmvz1wddb01c67clwe.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1mlmvz1wddb01c67clwe.gif" alt="Fourteen loose part PNGs assembling into a character that breathes, blinks and waves, rendered by rigc"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The player: posing a rig with DOM elements
&lt;/h2&gt;

&lt;p&gt;This is the part I think web developers will like. Every Spine web runtime rasterizes into a canvas, but the pose itself is computed on the CPU, and for a rigid part the bone transform is a plain affine map. CSS &lt;code&gt;transform: matrix()&lt;/code&gt; expresses that map exactly. So rig-c's player puts the rigid tier in the DOM: one absolutely positioned &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; per slot, a single &lt;code&gt;matrix()&lt;/code&gt; write per frame, composited by the browser, inspectable in devtools, styleable with CSS. Deforming meshes go to small per-part canvases; clipping is element-level &lt;code&gt;clip-path&lt;/code&gt;; a dirty-skip avoids writes for unchanged elements. The idea had come up on the Spine forum more than once since 2014; I did not find a usable implementation, so this is mine. There is also a one-canvas mode for when you want it.&lt;/p&gt;

&lt;p&gt;A minimal page is about twenty lines (this is the README's, trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"stage"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"position: relative; width: 480px; height: 640px; overflow: hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loadModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RigPlayer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.jsdelivr.net/npm/@rig-c/play@1/dist/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;modelUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rigs/look/skeleton.model.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RigPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientHeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cssText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`position: absolute; transform-origin: 0 0; transform: scale(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;);
    left: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px; top: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;syncPixelRatio&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one thing to know: &lt;code&gt;syncPixelRatio()&lt;/code&gt; after any scale change, or the mesh canvases raster at the unscaled rig's device pixel ratio and burn &lt;code&gt;1/scale²&lt;/code&gt; times more pixels than the screen shows (a uniform scale shrinks both axes, so at 0.25 that is 16×, not 4×). The README keeps a standing benchmark against &lt;code&gt;@esotericsoftware/spine-webgl 4.3.13&lt;/code&gt; with the machine, the Chromium build, the frame counts and the load guard written down.&lt;/p&gt;

&lt;p&gt;And in a game engine, the same model, with PixiJS 8 (from the getting-started guide):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Application&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pixi.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loadModel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@rig-c/play&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PixiRig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@rig-c/pixi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;backgroundAlpha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;modelUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/wave-build/skeleton.model.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PixiRig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;rig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wave&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage: from a painting and its See-through layers to a playing rig in about ten minutes, no GPU
&lt;/h2&gt;

&lt;p&gt;The getting-started guide (&lt;a href="https://rig-c.org/docs/getting-started/" rel="noopener noreferrer"&gt;https://rig-c.org/docs/getting-started/&lt;/a&gt;) starts from a published example whose painting and See-through layer split are already made, so the GPU steps are skipped entirely, and runs the rest with Bun alone, no Python. The page records its own timings on an Apple M4 with other jobs loading the machine: 536 seconds from an empty Bun cache, 571 seconds on a warm second run; most of that is the three builds. The shape of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add @rig-c/parts rig-c @rig-c/pose @rig-c/play
&lt;span class="c"&gt;# fetch the example painting + See-through layers (CC BY-NC 4.0, credited in the guide)&lt;/span&gt;
bunx rig-parts inputs   &lt;span class="nt"&gt;--source&lt;/span&gt; inputs/painting.png &lt;span class="nt"&gt;--config&lt;/span&gt; config.json &lt;span class="nt"&gt;--out&lt;/span&gt; work
bunx rig-parts assemble &lt;span class="nt"&gt;--source&lt;/span&gt; inputs/painting.png &lt;span class="nt"&gt;--full&lt;/span&gt; inputs/layers/full &lt;span class="nt"&gt;--head&lt;/span&gt; inputs/layers/head &lt;span class="nt"&gt;--config&lt;/span&gt; config.json &lt;span class="nt"&gt;--out&lt;/span&gt; work
bunx rig-parts propose  &lt;span class="nt"&gt;--parts&lt;/span&gt; work/rig &lt;span class="nt"&gt;--source&lt;/span&gt; inputs/painting.png &lt;span class="nt"&gt;--out&lt;/span&gt; work &lt;span class="nt"&gt;--keypoints&lt;/span&gt; keypoints.json
bunx rig-parts build    &lt;span class="nt"&gt;--config&lt;/span&gt; config.json &lt;span class="nt"&gt;--source&lt;/span&gt; inputs/painting.png &lt;span class="nt"&gt;--full&lt;/span&gt; inputs/layers/full &lt;span class="nt"&gt;--head&lt;/span&gt; inputs/layers/head &lt;span class="nt"&gt;--out&lt;/span&gt; out &lt;span class="nt"&gt;--loop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the guide shows you reading at each step is numbers, not adjectives: &lt;code&gt;recomposite vs source: mean |d|=1.13, within 8: 97.8%&lt;/code&gt; after assembly, &lt;code&gt;44 bone(s) — 29 checked and clean, 0 checked with a LINT line, 15 not checked&lt;/code&gt; after the bone proposal, and &lt;code&gt;check: PASS; 11 of 13 bar(s) measured, 2 skipped (FEET_STAY_LEVEL, STRESS_OFF_CHAIN)&lt;/code&gt; then &lt;code&gt;build: PASS&lt;/code&gt; at the end. If any line says FAIL, nothing was written, and that line says what to change.&lt;/p&gt;

&lt;p&gt;The guide's one hand correction is instructive about the "nothing is invented" rule. The pose script put the left ankle 12 px off the leg's art; &lt;code&gt;propose&lt;/code&gt; did not quietly move it, it printed a LINT naming the joint, the bone and the distance, and drafted the leg without an ankle cut. Moving the joint onto the shin by hand (one line) let &lt;code&gt;propose&lt;/code&gt; cut the shoes into two feet. A LINT is something to look at, not a refusal.&lt;/p&gt;

&lt;p&gt;Then a wave from joint positions, through &lt;code&gt;@rig-c/pose&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx rig-pose clean &lt;span class="nt"&gt;--pose&lt;/span&gt; wave.pose.json &lt;span class="nt"&gt;--out&lt;/span&gt; wave.pose.clean.json
bunx rig-pose retarget &lt;span class="nt"&gt;--rig&lt;/span&gt; out/rig &lt;span class="nt"&gt;--map&lt;/span&gt; map.json &lt;span class="nt"&gt;--pose&lt;/span&gt; wave.pose.clean.json &lt;span class="nt"&gt;--out&lt;/span&gt; wave.motion.json &lt;span class="nt"&gt;--name&lt;/span&gt; wave &lt;span class="nt"&gt;--profile&lt;/span&gt; spine-html &lt;span class="nt"&gt;--build&lt;/span&gt; wave-build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the whole thing plays in the browser with &lt;code&gt;@rig-c/play&lt;/code&gt;. The same build imports into a Spine 4.3 editor for a person to refine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that did not work, on the record
&lt;/h2&gt;

&lt;p&gt;Two things I decided early and would do again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trials are kept whether the answer was yes or no.&lt;/strong&gt; The examples repository has a &lt;code&gt;trials/&lt;/code&gt; directory with one rule for every entry: a question, the inputs and their origin, every command and pin, the measurement with its threshold stated before measuring, a verdict of PASS, FAIL or PARTIAL, the rejected alternatives, and the limits. A failed trial is as much a deliverable as a passing one; it is what stops the next attempt from repeating it. The README is explicit that a bar fitted to the result is not a bar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public numbers carry their failures beside them.&lt;/strong&gt; The core README's benchmark section reports that all eight numbered rungs of Spine's own example-project ladder clear the gate, with the thinnest margin written down (worst slot drift 5.5550 px against a 6.0 px bar). The next paragraph reports the harder series, from-zero attempts at the graduation rig with no inherited specs: 18.2, 18.8, 19.57, 7.86, 9.33 and 18.98 px, every one above the bar, with no monotone trend. The certification is scoped to "tool + guide + protocol across a bounded series of honest attempts," not to "an agent does it in one run from the brief," because the second has not been demonstrated.&lt;/p&gt;

&lt;p&gt;One limit the pose package states plainly: retargeting a single designed drawing through intense motion (a run, a dance) does not produce what the drawing lacks. Its README has a table of which approach fits which figure and motion, and a journey document of every lever tried on the way there with its measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exam: does the premise hold for a stranger?
&lt;/h2&gt;

&lt;p&gt;If the messages are the UI, then a fresh agent that has never seen the tools should be able to build a rig with nothing but what ships in the package: &lt;code&gt;README.md&lt;/code&gt;, &lt;code&gt;docs/&lt;/code&gt;, &lt;code&gt;--help&lt;/code&gt;. rig-c/exam tests exactly that. Before every command that changes a file, the examinee writes what it read and what it expects into a journal. Wherever expectation and reality differ, a tool message or guide was missing something, and those journal entries became issues.&lt;/p&gt;

&lt;p&gt;On 2026-10-20, eight models from three families sat all five tasks, each in a fresh session, each regraded by the proctor from the files it handed in. Thirteen independent sittings are on the board so far. The machine-graded lines on the dance task read whether the loop closes, the hands and trunk travel, the arms bend at their joints, she stays on her feet, the motion keeps the beat; they do not read whether it is a dance. So the proctor rendered each run's dance as a clip, lettered with no name, and one person judged all eight blind. The six runs that passed every line were scored from 3 to 9 out of 10. Passing the lines is what a run needs to be judged at all; it says almost nothing about the score. The report frames its own caveat: one sitting per model and one judge, so it is not a finding about the models.&lt;/p&gt;

&lt;p&gt;That is the loop I would recommend to anyone building tools for agents: write the refusal messages, then put a stranger in front of them and read the journal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live demo (20 rigs, bone and mesh overlays, no sign-up): &lt;a href="https://rig-c.org/examples/" rel="noopener noreferrer"&gt;https://rig-c.org/examples/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Getting started without a GPU or Python: &lt;a href="https://rig-c.org/docs/getting-started/" rel="noopener noreferrer"&gt;https://rig-c.org/docs/getting-started/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source, all MIT: &lt;a href="https://github.com/rig-c" rel="noopener noreferrer"&gt;https://github.com/rig-c&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The exam and its leaderboard: &lt;a href="https://rig-c.org/exam/" rel="noopener noreferrer"&gt;https://rig-c.org/exam/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Licensing, stated plainly: &lt;a href="https://rig-c.org/docs/licensing/" rel="noopener noreferrer"&gt;https://rig-c.org/docs/licensing/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The packages also ship Agent Skills (&lt;code&gt;skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;) so Claude Code, Codex, Gemini CLI or Antigravity can find the authoring rules without being told where they are.&lt;/p&gt;

&lt;p&gt;If you try it and a message leaves you not knowing what to change, that is a bug in the UI by this project's own definition. Please file it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: this article was drafted with AI assistance and edited and fact-checked by me against the project's public pages.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
