spine-rigc 0.35.0 โ†’ 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/AUTHORING.md CHANGED
@@ -1045,6 +1045,15 @@ page:
1045
1045
  | leaves a remaining **tie in the order your file declares it** | `turn` before `Turn`; `Mango` before `mango` โ€” each given in that order and returned in it |
1046
1046
  | writes **leaves then folders at the root**, and **sub-folders then leaves inside a folder** | `h` before `f/sub1/deep/q`; `f/sub1/x` before `f/leafA` |
1047
1047
 
1048
+ ๐Ÿ”ธ **One clause of the rule is not from those files.** Before comparing, the
1049
+ comparator reads U+3000 IDEOGRAPHIC SPACE as a space and the full-width digits
1050
+ U+FF10โ€“U+FF19 as `0`โ€“`9`. No probe carried either; the fold was measured on a
1051
+ production skin's slot keys ([#791](https://github.com/firejune/rigc/issues/791),
1052
+ ยง10.6b says what was measured and why it is those two classes and not NFKC), and
1053
+ it applies here because R10, R11 and a skin's slot keys are one comparator. So
1054
+ `shot๏ผ’` sorts before `shot10`, and a pair an ideographic space decides is
1055
+ ordered rather than refused as `separator`.
1056
+
1048
1057
  โญ **A tie is not an ambiguity, and that is what retired most of this rule's
1049
1058
  refusals.** Two names the comparator cannot separate come back in the order the
1050
1059
  file gave them, so the order rigc emits for such a pair is **your own declaration
@@ -8161,6 +8170,33 @@ says which.**
8161
8170
  t=0, so the editor wrote them `value` alone, and that rule would have moved
8162
8171
  `time` โ€” first in every other key kind the editor writes โ€” behind `value`.
8163
8172
 
8173
+ ๐Ÿ”ฌ **Two positions and one fold are measured on a production set, not on
8174
+ `examples/`** ([#791](https://github.com/firejune/rigc/issues/791)). The twelve
8175
+ exports carry no mesh `path` or `color` and no slot name outside ASCII โ€” `IG84`
8176
+ and `IG85` count both on every run with a corpus โ€” so the table cannot learn
8177
+ either, and what rigc writes for them was read off 42 production exports
8178
+ instead:
8179
+
8180
+ - **A mesh attachment writes `path` and `color` right after `type`, `path` before `color` โ€” measured on a production set (#791), not on `examples/`.**
8181
+ 18 meshes whose `path` differs from their name wrote it second, and 1 with a
8182
+ `color` wrote that second; no mesh carried both. The row does not list them,
8183
+ so the position is the constructor's โ€” `meshTextureKeys` in
8184
+ [`src/compile.ts`](../src/compile.ts), which every mesh route spreads right
8185
+ after `type` (`S104`). A region's `path` and a linked mesh's keys stay where
8186
+ they were: neither was in the set, and an analogy is not a measurement.
8187
+ - **A skin's slot keys are compared after `foldBeforeComparing`, which reads U+3000 as U+0020 and U+FF10โ€“U+FF19 as U+0030โ€“U+0039** โ€”
8188
+ the ideographic space as a space, the full-width digits as `0`โ€“`9`
8189
+ (`EDITOR_NAME_FOLD`). One map of 95 slot keys, 7 of them carrying those
8190
+ characters, came back in an order this reproduces 95 of 95 and neither a
8191
+ codepoint sort nor the comparator without the fold does (`S105`). It is **not**
8192
+ `normalize('NFKC')`, though NFKC folds both the same way: NFKC, a classifier
8193
+ that reads every Unicode digit as a digit and every space separator as a space,
8194
+ and a width fold alone all reproduce that map and disagree past it (on `๏ฌ`,
8195
+ `ยฒ`, `ูฃ`, `๏ผก`, a no-break space), and what all three agree on is exactly
8196
+ these two classes. The comparator is R10's, so animation and skin names are
8197
+ compared after the fold too. A pair the fold does not close โ€” a tab, a
8198
+ no-break space โ€” still keeps the map in rigc's order, whole (`S106`).
8199
+
8164
8200
  ๐Ÿ”ธ **An object keyed by NAMES is not a field order**, and the table does not
8165
8201
  touch one. A bone's or a slot's timelines, and a transform constraint's
8166
8202
  `properties` and each `to` inside them, are read into the runtime's arrays in the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "description": "Rig compiler for Spine โ€” declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/compile.ts CHANGED
@@ -400,7 +400,11 @@ function editorSkinOrder<T extends { name: string }>(skins: readonly T[]): T[] {
400
400
  * partly sorted:
401
401
  *
402
402
  * - a pair the comparator leaves open (`editorNameOrder`'s number, separator and
403
- * folder cases);
403
+ * folder cases). Since #791 a name carrying U+3000 or a full-width digit is
404
+ * not one: the comparator folds both first (`EDITOR_NAME_FOLD`), which is
405
+ * what a production map of 95 slot keys was measured to need, so such a map
406
+ * is sorted where it used to be kept whole โ€” or, with only full-width digits
407
+ * in it, sorted with the digits at their code points;
404
408
  * - any slot name holding `/`. The folder rule was measured on skin and
405
409
  * animation names, which the editor files in folders; slots it files under
406
410
  * bones, and no round trip carried a slot name with a `/` in it.
@@ -465,10 +469,59 @@ interface ReadName {
465
469
  skipped: number;
466
470
  }
467
471
 
472
+ /**
473
+ * The characters the editor folds before it compares two names, as
474
+ * `[first, last, to]` code point ranges: each of `first`โ€ฆ`last` is read as
475
+ * `to` plus its offset from `first` (issue #791).
476
+ *
477
+ * ๐Ÿ”ฌ **Measured on a production set, not on the probes or `examples/`.** One
478
+ * skin's `attachments` map of 95 slot keys, 7 of them carrying U+3000
479
+ * IDEOGRAPHIC SPACE or a full-width digit (U+FF12, U+FF14), came back in an
480
+ * order this comparator reproduces **95 of 95** once U+3000 is read as U+0020
481
+ * and U+FF10โ€“U+FF19 as `0`โ€“`9` โ€” an order neither a plain codepoint sort nor a
482
+ * case-insensitive sort without the fold reproduces. No name in the five probes, the
483
+ * twelve exports or any spec in this tree carries either character, so the set
484
+ * is the only measurement of them there is.
485
+ *
486
+ * โš ๏ธ **Exactly those two classes, and not `normalize('NFKC')`, although NFKC
487
+ * folds both the same way.** Three readings reproduce the measurement and
488
+ * disagree past it: NFKC (which also turns `๏ฌ` into `fi`, `ยฒ` and `โ‘ ` into
489
+ * digits, U+00A0 and U+2000โ€“U+200A into a space, and composes `e` + U+0301
490
+ * into `รฉ`); a classifier that reads every Unicode decimal digit as a digit and
491
+ * every space separator as a space (which also reads `ูฃ` as 3, and does none
492
+ * of NFKC's other folds); and a width fold alone (which also reads `๏ผก` as `A`,
493
+ * and folds no other space). What all three agree on is exactly U+3000 and the
494
+ * ten full-width digits โ€” the ten are one class under every reading, so the
495
+ * eight the set did not carry are folded with the two it did โ€” and that
496
+ * intersection is what is applied. Every other character is compared at its own
497
+ * code point as before, and a whitespace character that is not a space is still
498
+ * `leafOrder`'s `separator`.
499
+ *
500
+ * One comparator (#728), so the fold reaches `skins` and `animations` too: an
501
+ * animation name with a full-width digit was compared at the digit's code point,
502
+ * the order the only measurement of such a name contradicts, and a pair that
503
+ * U+3000 decided was refused as `separator` and is now ordered.
504
+ */
505
+ export const EDITOR_NAME_FOLD: ReadonlyArray<readonly [number, number, number]> = [
506
+ [0x3000, 0x3000, 0x0020],
507
+ [0xff10, 0xff19, 0x0030],
508
+ ];
509
+
510
+ /** `name` with every character `EDITOR_NAME_FOLD` covers read as the character it folds to. */
511
+ function foldBeforeComparing(name: string): string {
512
+ let out = '';
513
+ for (const char of name) {
514
+ const code = char.codePointAt(0) ?? 0;
515
+ const range = EDITOR_NAME_FOLD.find(([first, last]) => code >= first && code <= last);
516
+ out += range === undefined ? char : String.fromCodePoint(range[2] + code - range[0]);
517
+ }
518
+ return out;
519
+ }
520
+
468
521
  function readName(name: string, skip: RegExp): ReadName {
469
522
  const chars: string[] = [];
470
523
  let skipped = 0;
471
- for (const char of foldUp(name)) {
524
+ for (const char of foldUp(foldBeforeComparing(name))) {
472
525
  if (skip.test(char)) skipped++;
473
526
  else chars.push(char);
474
527
  }
@@ -4292,6 +4345,33 @@ function attachmentPath(att: { path?: string; image?: string }, placeholder: str
4292
4345
  return region === placeholder ? undefined : region;
4293
4346
  }
4294
4347
 
4348
+ /**
4349
+ * A mesh attachment's `path` and `color`, in that order โ€” the two keys every
4350
+ * mesh constructor spreads right after `type` (issue #791).
4351
+ *
4352
+ * ๐Ÿ”ฌ The editor writes a mesh `type, path, color, uvs, โ€ฆ`: right after `type`,
4353
+ * `path` before `color` โ€” measured on a production set (#791), not on
4354
+ * `examples/`. There 18 meshes whose `path` differs from their name wrote it
4355
+ * second and 1 mesh with a `color` wrote that second; no object carried both, so
4356
+ * `path` before `color` is the order the two measured positions leave, not a
4357
+ * third measurement. The twelve exports carry neither key on a mesh, which is
4358
+ * why `EDITOR_KEY_ORDER`'s `mesh attachment` row does not list them: an unlisted
4359
+ * key keeps its constructor's index, so the position is stated HERE, and the
4360
+ * row stays what the public exports derive.
4361
+ *
4362
+ * โš ๏ธ Not a linked mesh's, and not a region's. Neither was in the measured set โ€”
4363
+ * a region's `path` stays where `buildRigRegion` puts it, a linked mesh's
4364
+ * keys where `buildRigLinkedMesh` does โ€” because the analogy to a mesh is
4365
+ * obvious and it is not a measurement.
4366
+ */
4367
+ function meshTextureKeys(att: { path?: string; image?: string; color?: string }, placeholder: string): Pick<SpineMeshAttachment, 'path' | 'color'> {
4368
+ const out: Pick<SpineMeshAttachment, 'path' | 'color'> = {};
4369
+ const path = attachmentPath(att, placeholder);
4370
+ if (path !== undefined) out.path = path;
4371
+ if (att.color !== undefined) out.color = att.color;
4372
+ return out;
4373
+ }
4374
+
4295
4375
  /**
4296
4376
  * The atlas region frame `i` of a sequence resolves to โ€” `Sequence.getPath`
4297
4377
  * (`Sequence.js:124-132`) transcribed: the stem, then `start + i` left-padded
@@ -4707,6 +4787,7 @@ function buildRigMesh(
4707
4787
  }
4708
4788
  const out: SpineMeshAttachment = {
4709
4789
  type: 'mesh',
4790
+ ...meshTextureKeys(att, placeholder),
4710
4791
  uvs: att.uvs.map(f32),
4711
4792
  triangles: att.triangles,
4712
4793
  vertices,
@@ -4715,9 +4796,6 @@ function buildRigMesh(
4715
4796
  width: f32(width),
4716
4797
  height: f32(height),
4717
4798
  };
4718
- const path = attachmentPath(att, placeholder);
4719
- if (path !== undefined) out.path = path;
4720
- if (att.color !== undefined) out.color = att.color;
4721
4799
  if (att.sequence !== undefined) out.sequence = emitSequence(att.sequence);
4722
4800
  // Register it as `authored`: geometry rigc did not build and whose topology it
4723
4801
  // therefore gets to assume nothing about. The generator-topology assertions
@@ -4998,6 +5076,7 @@ function buildGeneratedMesh(
4998
5076
  });
4999
5077
  const out: SpineMeshAttachment = {
5000
5078
  type: 'mesh',
5079
+ ...meshTextureKeys(att, placeholder),
5001
5080
  uvs: geometry.uvs.map(f32),
5002
5081
  triangles: geometry.triangles,
5003
5082
  vertices: vertices.map(f32),
@@ -5005,9 +5084,6 @@ function buildGeneratedMesh(
5005
5084
  width: f32(w),
5006
5085
  height: f32(h),
5007
5086
  };
5008
- const path = attachmentPath(att, placeholder);
5009
- if (path !== undefined) out.path = path;
5010
- if (att.color !== undefined) out.color = att.color;
5011
5087
  return out;
5012
5088
  }
5013
5089
 
@@ -5565,6 +5641,7 @@ function buildGridAttachment(
5565
5641
  });
5566
5642
  const out: SpineMeshAttachment = {
5567
5643
  type: 'mesh',
5644
+ ...meshTextureKeys(att, placeholder),
5568
5645
  uvs: geometry.uvs.map(f32),
5569
5646
  triangles: geometry.triangles,
5570
5647
  vertices: vertices.map(f32),
@@ -5572,9 +5649,6 @@ function buildGridAttachment(
5572
5649
  width: f32(w),
5573
5650
  height: f32(h),
5574
5651
  };
5575
- const path = attachmentPath(att, placeholder);
5576
- if (path !== undefined) out.path = path;
5577
- if (att.color !== undefined) out.color = att.color;
5578
5652
  return out;
5579
5653
  }
5580
5654
 
@@ -5726,8 +5800,15 @@ function buildContourAttachment(
5726
5800
  holePixels: geometry.contour?.holePixels,
5727
5801
  depth: depth?.summary,
5728
5802
  });
5803
+ // Same rule a region attachment follows: the atlas region is the PNG's
5804
+ // basename, so a placeholder named anything else needs `path` written down or
5805
+ // the loader resolves nothing. Stated once in `attachmentPath` since #577 โ€”
5806
+ // this comment used to be the rule's only statement, beside four emit sites
5807
+ // that disagreed with it โ€” and spread right after `type` by
5808
+ // `meshTextureKeys` since #791.
5729
5809
  const out: SpineMeshAttachment = {
5730
5810
  type: 'mesh',
5811
+ ...meshTextureKeys(att, placeholder),
5731
5812
  uvs: geometry.uvs.map(f32),
5732
5813
  triangles: geometry.triangles,
5733
5814
  vertices: vertices.map(f32),
@@ -5735,14 +5816,6 @@ function buildContourAttachment(
5735
5816
  width: f32(w),
5736
5817
  height: f32(h),
5737
5818
  };
5738
- // Same rule a region attachment follows: the atlas region is the PNG's
5739
- // basename, so a placeholder named anything else needs `path` written down or
5740
- // the loader resolves nothing. Stated once in `attachmentPath` since #577 โ€”
5741
- // this comment used to be the rule's only statement, beside four emit sites
5742
- // that disagreed with it.
5743
- const path = attachmentPath(att, placeholder);
5744
- if (path !== undefined) out.path = path;
5745
- if (att.color !== undefined) out.color = att.color;
5746
5819
  return out;
5747
5820
  }
5748
5821
 
package/src/types.ts CHANGED
@@ -888,7 +888,9 @@ export interface SpineMeshAttachment {
888
888
  type: 'mesh';
889
889
  /** See `SpineRegionAttachment.name` โ€” and it takes `path` with it. */
890
890
  name?: string;
891
+ /** `path` and `color` are written right after `type` โ€” `compile.ts`'s `meshTextureKeys` says where that is measured. */
891
892
  path?: string;
893
+ color?: string;
892
894
  uvs: number[];
893
895
  triangles: number[];
894
896
  /** Weighted encoding: boneCount, (boneIndex, bindX, bindY, weight)*n, repeated. */
@@ -910,7 +912,6 @@ export interface SpineMeshAttachment {
910
912
  /** Nonessential, but they make the mesh budget assertions readable. */
911
913
  width: number;
912
914
  height: number;
913
- color?: string;
914
915
  sequence?: SpineSequence;
915
916
  }
916
917