spine-rigc 0.36.0 → 1.0.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/src/compile.ts CHANGED
@@ -95,6 +95,7 @@ import {
95
95
  pageFootprint,
96
96
  pageGridSaid,
97
97
  pageGridSentence,
98
+ canonicalAtlasShape,
98
99
  parseAtlasText,
99
100
  rewritePageNames,
100
101
  writeAtlasText,
@@ -2463,6 +2464,14 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2463
2464
  // for — `scale:` most expensively — so the text passes through by line and only
2464
2465
  // the name lines are replaced (`rewritePageNames`).
2465
2466
  //
2467
+ // 🔸 Its BLANK lines are the one thing put into rigc's shape
2468
+ // (`canonicalAtlasShape`, issue #803): none before the first entry, one between
2469
+ // blocks, one trailing newline. The runtime reads any run of blank lines as
2470
+ // one, and a 3.8-era packer opens every file with one, which `A07` read at
2471
+ // `line 1` and refused — the gate refusing a whitespace choice of a packer
2472
+ // rigc re-anchored, on a pack every 4.x runtime loads. Non-blank lines are
2473
+ // untouched, so a pack already in that shape is emitted byte for byte as before.
2474
+ //
2466
2475
  // ⚠️ Regions the rig does not use stay in the emitted atlas. They are not a
2467
2476
  // defect: a real pack is shared between cuts, an unused region costs a consumer
2468
2477
  // nothing, and dropping them would make `--out` disagree with the pack it was
@@ -2470,8 +2479,8 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2470
2479
  const atlasText =
2471
2480
  atlasIn === null
2472
2481
  ? buildAtlasText(images)
2473
- : rewritePageNames(atlasIn.parsed, (name) =>
2474
- relative(outDir, resolve(atlasIn.dir, name)).split('\\').join('/'),
2482
+ : canonicalAtlasShape(
2483
+ rewritePageNames(atlasIn.parsed, (name) => relative(outDir, resolve(atlasIn.dir, name)).split('\\').join('/')),
2475
2484
  );
2476
2485
 
2477
2486
  // -- 3. bones --------------------------------------------------------------
@@ -2523,10 +2532,32 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2523
2532
  };
2524
2533
  /** Linked meshes to resolve once every skin exists — see `resolveLinkedMeshes`. */
2525
2534
  const pendingLinks: PendingLink[] = [];
2526
- tableFor('default'); // rigc always emits a default skin, even when it is empty
2527
- // ...and every skin the rig declares, for the same reason: a skin can now carry
2528
- // `bones`/constraint lists with no attachments at all, and a skin that only
2529
- // switches bones on would otherwise never reach the emitted array.
2535
+ // 🔑 **A `default` skin is emitted exactly when the spec has one** (issue #801):
2536
+ // the rig spec's `skins` carries the key — empty or not — or a manifest part
2537
+ // files its states under it (the loop below). Otherwise there is none, which is
2538
+ // the shape the editor's own export has: a rig whose art all lives in named
2539
+ // skins came back from 4.3.26 with `skins` 3 → 2, the empty `default` gone,
2540
+ // and `SkeletonBinary.readSkin` returns null for a default skin with no slots,
2541
+ // so the binary half cannot even hold one. The runtime treats the two
2542
+ // spellings alike — `SkeletonData.defaultSkin` is null or an empty skin, and
2543
+ // `Skeleton.getAttachment` finds nothing in either (`Skeleton.js:335-346`).
2544
+ // This line used to create it unconditionally, "even when it is empty", which
2545
+ // made every rebuild of such an export differ from it by one skin — a value
2546
+ // the spec never stated.
2547
+ //
2548
+ // Created HERE, before the slot loop, rather than lazily by the first part
2549
+ // that fills it, so `skinTables` keeps `default` first whichever of the two
2550
+ // put it there, and every refusal that lists the emitted skins lists them in
2551
+ // the order it did before.
2552
+ const partsFillDefault = rig.slots.some(
2553
+ (rigSlot) =>
2554
+ partBySlot.has(rigSlot.name) &&
2555
+ (slotAttachments.get(rigSlot.name) ?? rigAttachmentNames.get(rigSlot.name) ?? []).length > 0,
2556
+ );
2557
+ if (skinNames.includes(DEFAULT_SKIN) || partsFillDefault) tableFor(DEFAULT_SKIN);
2558
+ // ...and every skin the rig declares: a skin can carry `bones`/constraint lists
2559
+ // with no attachments at all, and a skin that only switches bones on would
2560
+ // otherwise never reach the emitted array.
2530
2561
  for (const skinName of skinNames) tableFor(skinName);
2531
2562
  const meshBones = new Set<string>();
2532
2563
  const meshes: CompileResult['meshes'] = [];
@@ -4028,6 +4059,17 @@ function setupWorldVertices(
4028
4059
  * two are the outer control handles of the end knots, which no curve uses. A
4029
4060
  * closed path instead rotates by one and repeats the first knot at the end.
4030
4061
  * Either way what comes out is a `3K + 1` chain: knot, handle, handle, knot, …
4062
+ *
4063
+ * ⭐ **`lengths` is measured over the CLOSED chain whether the path is closed or
4064
+ * not** (issue #804), because that is the array the format holds: the parser
4065
+ * sizes it `vertexCount / 3` on both (`SkeletonJson.js:601`), which on an open
4066
+ * path is one more than its curves, and the editor fills that last entry with
4067
+ * the wrap-around curve's cumulative — measured on a 4.3 production export, all
4068
+ * four of whose entries the closed chain over the runtime's own posed vertices
4069
+ * reproduces to the digit it prints. The open chain is the closed chain's
4070
+ * prefix, so the entries the runtime reads are the same numbers either way; the
4071
+ * trailing one is read by nothing, and is written so a rebuild is the file the
4072
+ * editor writes rather than one entry short of it.
4031
4073
  */
4032
4074
  function pathChain(points: Array<[number, number]>, closed: boolean): Array<[number, number]> {
4033
4075
  if (!closed) return points.slice(1, points.length - 1);
@@ -4050,11 +4092,33 @@ function pathChain(points: Array<[number, number]>, closed: boolean): Array<[num
4050
4092
  * — `0.1875 = 3t²`, `0.09375 = 6t³`, `0.75 = 3t`, `0.16666667` standing in for
4051
4093
  * 1/6 — accumulating four `Math.sqrt` terms per curve into a running
4052
4094
  * `pathLength`, and writing the running value into `curves[i]` at each curve's
4053
- * end. The Spine editor's exported `lengths` are that same computation: measured
4054
- * against two editor exports, one open path from 4.3.23 and one closed path from
4055
- * 4.3.26, this reproduces every digit the editor printed. So the loop below is a
4056
- * transcription, not a sampler that happens to agree — and the two are not the
4057
- * same thing, which is the reason the transcription is here.
4095
+ * end. The Spine editor's exported `lengths` are that same computation, and the
4096
+ * loop below is a transcription of it, not a sampler that happens to agree.
4097
+ *
4098
+ * 🚨 **What that computation is fed decides the number, and this comment once
4099
+ * claimed more than was measured** (issue #804). It said the loop matched the
4100
+ * editor to every digit it printed — true on the two exports it was measured on,
4101
+ * one open and one closed path on unweighted, unscaled, unconstrained rigs, and
4102
+ * false in production on two counts:
4103
+ *
4104
+ * - **Geometry.** The editor measures the world positions of the posed
4105
+ * vertices. rigc blends weighted vertices through its own setup transforms,
4106
+ * which until #804 ignored bone scale, shear and `inherit` — 2.35× the
4107
+ * runtime's own `curves` on a production rig's weighted path, 0.752× on a
4108
+ * 50/50 probe over a bone at scale 2. `computeWorldTransforms` is now the
4109
+ * runtime's, and the gap is gone on both.
4110
+ * - **Constraints.** The runtime measures the pose its update order hands the
4111
+ * path constraint at the first `updateWorldTransform`: a transform
4112
+ * constraint on the path's slot bone ordered BEFORE the path constraint
4113
+ * moves the curves (416.14 → 551.59 on a probe), one ordered after does
4114
+ * not. A production 4.2 export's four numbers are the constrained pose's to
4115
+ * the digit, and its unconstrained setup measures 76.08 against 76.65 on the
4116
+ * first curve. Reproducing that means solving every constraint type in
4117
+ * update order, which is posing — and `src/compile.ts` does not link the
4118
+ * runtime. ⇒ **An omitted `lengths` is measured on the unconstrained setup
4119
+ * pose**, and a path whose bones a constraint moves at rest gets that
4120
+ * figure. What closes the gap for an editor export is not measuring at all:
4121
+ * a stated `lengths` is carried (`buildRigPath`).
4058
4122
  *
4059
4123
  * ⚠️ rigc measured this with a 64-chord sum until issue #560, and the comment
4060
4124
  * that stood here argued the difference was inside anything's tolerance. It was
@@ -4065,7 +4129,7 @@ function pathChain(points: Array<[number, number]>, closed: boolean): Array<[num
4065
4129
  * two apart and since issue #716 rigc's own file can only where the two land on
4066
4130
  * either side of a float's boundary. Under the six-decimal rounding that stood
4067
4131
  * until then it could: on both measured rigs the two spellings differed on the
4068
- * LAST curve, where the accumulated difference is largest. `PS67`–`PS69` in `selftest.ts` compare
4132
+ * LAST curve, where the accumulated difference is largest. `PS67`, `PS68` and `PS186` in `selftest.ts` compare
4069
4133
  * this against `PathConstraint`'s own `curves` array read off a posed skeleton,
4070
4134
  * which is the only oracle that can see that gap.
4071
4135
  *
@@ -4118,20 +4182,17 @@ function pathCurveLengths(chain: Array<[number, number]>): number[] {
4118
4182
  * groups of six then straddle the knots, and bones slide along a curve
4119
4183
  * nobody drew. Too few points is the same failure with fewer symptoms — an
4120
4184
  * open path needs 6 for one curve, a closed one 3.
4121
- * 2. **`lengths` is measured, not copied.** See `RigPathAttachment`: it is the
4122
- * setup arc length of the geometry two fields above it, and a restated
4123
- * number that disagrees is only visible under `constantSpeed: false`, where
4124
- * it silently rescales the whole traversal.
4125
- * 3. **An authored `lengths` is refused**, for that reason.
4185
+ * 2. **A stated `lengths` is carried as stated** (issue #804), and checked for
4186
+ * the one shape the parser cannot hold: `vertexCount / 3` entries, each
4187
+ * finite and none below the one before it. The file is the record of what
4188
+ * was measured, and the number an editor export states is the editor's
4189
+ * measurement of a pose rigc cannot reproduce without posing (see
4190
+ * `pathCurveLengths`) — so re-deriving it moved a production rig's bones by
4191
+ * up to 0.0054 in issue #804's pose comparison, where carrying it moved none.
4192
+ * 3. **An omitted one is measured**, `vertexCount / 3` entries over the closed
4193
+ * chain (`pathChain`), on the unconstrained setup pose.
4126
4194
  */
4127
4195
  function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentContext): SpinePathAttachment {
4128
- if (att.lengths !== undefined) {
4129
- throw new CompileError(
4130
- `${where}: "lengths" is not authored — rigc measures the setup arc length of each curve off the geometry, the ` +
4131
- 'same way it measures a region\'s size off its PNG. A restated length that disagrees with the vertices is ' +
4132
- 'invisible until `constantSpeed` is false, and then it rescales the whole traversal in silence.',
4133
- );
4134
- }
4135
4196
  const vertices = buildVertexGeometry(att, where, ctx);
4136
4197
  const count = att.vertexCount;
4137
4198
  const closed = att.closed === true;
@@ -4149,15 +4210,25 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
4149
4210
  `${closed ? 'a closed path of K curves carries 3K points' : 'an open one carries 3(K + 1), the first and last being the end knots\' outer handles'}`,
4150
4211
  );
4151
4212
  }
4152
- const anchor = ctx.transforms.get(ctx.anchorBone);
4153
- if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
4154
- const points = setupWorldVertices(vertices, count, anchor, ctx.bones, ctx.transforms, where);
4155
- const lengths = pathCurveLengths(pathChain(points, closed));
4156
- if (!lengths.length || !lengths.every((n) => Number.isFinite(n)) || lengths[lengths.length - 1] <= 0) {
4157
- throw new CompileError(
4158
- `${where}: the geometry measures ${lengths.length} curve(s) of total length ${String(lengths[lengths.length - 1])}; ` +
4159
- 'a path of zero length divides by zero the first time a bone is placed on it',
4160
- );
4213
+ const entries = count / 3;
4214
+ let lengths: number[];
4215
+ if (att.lengths !== undefined) {
4216
+ lengths = statedPathLengths(att.lengths, entries, closed, where);
4217
+ } else {
4218
+ const anchor = ctx.transforms.get(ctx.anchorBone);
4219
+ if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
4220
+ const points = setupWorldVertices(vertices, count, anchor, ctx.bones, ctx.transforms, where);
4221
+ lengths = pathCurveLengths(pathChain(points, true));
4222
+ // The total the runtime reads is the last CURVE's entry, `lengths[vertexCount
4223
+ // / 3 - (closed ? 1 : 2)]` (`PathConstraint.js:204-206`) — on an open path
4224
+ // the one before the trailing wrap-around entry, which nothing reads.
4225
+ const total = lengths[closed ? entries - 1 : entries - 2];
4226
+ if (lengths.length !== entries || !lengths.every((n) => Number.isFinite(n)) || !(total > 0)) {
4227
+ throw new CompileError(
4228
+ `${where}: the geometry measures ${lengths.length} entry(ies) where the parser sizes ${entries}, of total ` +
4229
+ `length ${String(total)}; a path of zero length divides by zero the first time a bone is placed on it`,
4230
+ );
4231
+ }
4161
4232
  }
4162
4233
  // Field order is the parser's reading order (`:606-623`), and each optional key
4163
4234
  // is present exactly when the spec declared it — the rule the whole rig spec
@@ -4173,6 +4244,50 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
4173
4244
  };
4174
4245
  }
4175
4246
 
4247
+ /**
4248
+ * A stated `lengths`, checked for the shape the parser needs and returned as
4249
+ * stated (issue #804).
4250
+ *
4251
+ * 🔒 **The count is the parser's, not the constraint's.** `SkeletonJson.js:601`
4252
+ * allocates `vertexCount / 3` entries on an open path and a closed one alike and
4253
+ * copies what the file gives: a SHORT array leaves zeros the constraint then
4254
+ * reads as a curve's end (a position divided by 0, in silence), and a LONG one
4255
+ * grows the array past what the attachment's own geometry has. Both are refused
4256
+ * with the two counts rather than trimmed or padded, because either repair is a
4257
+ * value the spec did not state.
4258
+ *
4259
+ * ⚠️ Equal neighbours are accepted here and refused by `A33` where the runtime
4260
+ * reads them — a zero-length curve is a fact about the pose, which the gate
4261
+ * measures; a DECREASING entry is not a cumulative length at all, on any pose.
4262
+ */
4263
+ function statedPathLengths(stated: unknown, entries: number, closed: boolean, where: string): number[] {
4264
+ if (!Array.isArray(stated)) {
4265
+ throw new CompileError(`${where}: "lengths" is ${JSON.stringify(stated)}; it is an array of ${entries} number(s)`);
4266
+ }
4267
+ if (stated.length !== entries) {
4268
+ throw new CompileError(
4269
+ `${where}: "lengths" has ${stated.length} entry(ies) where the parser sizes ${entries} — vertexCount / 3 on ` +
4270
+ `${closed ? 'a closed' : 'an open'} path${closed ? '' : ', one more than its curves: the editor writes the wrap-around curve\'s cumulative last and nothing reads it'}. ` +
4271
+ 'Give the array the source states, or leave the field out and rigc measures it',
4272
+ );
4273
+ }
4274
+ let previous = 0;
4275
+ for (let i = 0; i < stated.length; i++) {
4276
+ const value: unknown = stated[i];
4277
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
4278
+ throw new CompileError(`${where}: "lengths"[${i}] is ${JSON.stringify(value)}; every entry is a finite cumulative length`);
4279
+ }
4280
+ if (value < previous) {
4281
+ throw new CompileError(
4282
+ `${where}: "lengths"[${i}] is ${value}, below ${i === 0 ? '0' : `the ${previous} before it`}. The array is CUMULATIVE — the length at ` +
4283
+ 'the end of each curve — so no entry is below its predecessor',
4284
+ );
4285
+ }
4286
+ previous = value;
4287
+ }
4288
+ return stated as number[];
4289
+ }
4290
+
4176
4291
  /**
4177
4292
  * The compiled image an attachment's `image` names — or a refusal that names the
4178
4293
  * file and says what is actually wrong with it.
@@ -4857,13 +4972,21 @@ function resolveLinkedMeshes(
4857
4972
  ): void {
4858
4973
  const skinNames = [...tables.keys()];
4859
4974
  for (const link of links) {
4860
- // ⚠️ Only a STATED `skin` can miss here, and it is worth saying why rather
4861
- // than leaving the other half to look like a branch nothing reaches: rigc
4862
- // always emits a `default` skin, empty if it has to (`tableFor('default')`),
4863
- // so the parser's own default always resolves to a table. An omitted `skin`
4864
- // therefore fails one line down, at the source, and the sentence there is
4865
- // what names the trap.
4975
+ // ⚠️ An omitted `skin` can miss here too since issue #801: rigc emits a
4976
+ // `default` skin only when the spec has one, and the parser's default for a
4977
+ // link is `skeletonData.defaultSkin`, which is then null — the runtime reads
4978
+ // `getAttachment` off it and throws a `TypeError` naming neither. So the
4979
+ // omitted case is said separately, because "the skin is default" would be a
4980
+ // sentence about a value nobody wrote.
4866
4981
  const table = tables.get(link.skin);
4982
+ if (table === undefined && !link.skinStated) {
4983
+ throw new CompileError(
4984
+ `${link.where}: no "skin" is stated, so the parser looks for the source "${link.source}" in the default ` +
4985
+ `skin — and this rig declares no default skin. The rig's skins are ${skinNames.map((s) => JSON.stringify(s)).join(', ')}. ` +
4986
+ 'State the skin the source is filed under. Left to the round trip this is a `TypeError` off the null ' +
4987
+ '`SkeletonData.defaultSkin`, which names neither this attachment nor where it was looking.',
4988
+ );
4989
+ }
4867
4990
  if (table === undefined) {
4868
4991
  throw new CompileError(
4869
4992
  `${link.where}: "skin" is ${JSON.stringify(link.skin)}, and the rig declares no such skin. The rig's skins ` +
@@ -7189,8 +7312,8 @@ function deformGeometryOf(
7189
7312
  //
7190
7313
  // - `lengths` is a field of the ATTACHMENT. The format has nowhere to put
7191
7314
  // a per-key length, so no export of any tool carries a re-measured one;
7192
- // the editor's own is the setup measurement, which is what
7193
- // `pathCurveLengths` reproduces digit for digit.
7315
+ // the editor's own is the setup measurement, which a stated array
7316
+ // carries and `pathCurveLengths` measures when none is stated.
7194
7317
  // - `PathConstraint.computeWorldPositions` reads that field only under
7195
7318
  // `constantSpeed: false` (`PathConstraint.js:205`). Under the parser's
7196
7319
  // default, `true`, it re-measures the curve from the posed world
package/src/ingest.ts CHANGED
@@ -124,8 +124,8 @@ export class IngestSpecRefused extends Error {
124
124
  * - `judgement` — the skeleton does not carry it and somebody decided. There
125
125
  * are exactly two: a stage the caller supplied, and an animation's duration.
126
126
  * - `lossy` — the skeleton's spelling and rigc's differ, on purpose, and the
127
- * difference is named: a value rigc re-derives rather than takes (`lengths`,
128
- * the `spine` version), a field the spec has no home for (`hash`), or
127
+ * difference is named: a value rigc re-derives rather than takes (the
128
+ * `spine` version), a field the spec has no home for (`hash`), or
129
129
  * a default the source left to the format and the rebuild writes out
130
130
  * (`HEADER_ORIGIN`, issue #622). The rebuilt file is a different file in that
131
131
  * field; it is not a different rig.
@@ -1407,17 +1407,12 @@ function ingestAttachment(
1407
1407
  out.vertexCount = att.vertexCount;
1408
1408
  geometry(vertexCount);
1409
1409
  for (const field of ['closed', 'constantSpeed', 'color']) if (att[field] !== undefined) out[field] = att[field];
1410
- if (att.lengths !== undefined) {
1411
- note(
1412
- 'lossy',
1413
- 'PATH_LENGTHS',
1414
- at.where,
1415
- 'the source states `lengths`; the rig spec refuses an authored one and rigc RE-MEASURES it as ' +
1416
- '`PathConstraint` does (issue #560, `pathCurveLengths`). Dropping it is correct: the field is the ' +
1417
- "runtime's own four-sample forward difference, not an arc length, and a transcribed one would freeze " +
1418
- 'whatever produced the source',
1419
- );
1420
- }
1410
+ // Carried verbatim (issue #804), where until then it was dropped as `LOSS
1411
+ // PATH_LENGTHS` and re-measured. The editor measures it on the pose the
1412
+ // first update gives the path constraint — constraints applied — and rigc
1413
+ // does not pose, so the re-measure was a different number on every path a
1414
+ // constraint moves at rest, and on every weighted path over a scaled bone.
1415
+ if (att.lengths !== undefined) out.lengths = att.lengths;
1421
1416
  } else {
1422
1417
  note(
1423
1418
  'blocker',
package/src/rig.ts CHANGED
@@ -827,7 +827,7 @@ export interface RigLinkedMeshAttachment {
827
827
  sequence?: RigSequence;
828
828
  /**
829
829
  * 🚫 Every geometry field a mesh may state, refused by name on a link. They
830
- * are declared for the reason `RigPathAttachment.lengths` is: a key the shape
830
+ * are declared so the refusal can name them: a key the shape
831
831
  * does not hold at all comes back as *keys this compiler does not read … fix
832
832
  * the spelling or remove it*, and the remedy sentence is wrong here — the
833
833
  * fault is not a typo, it is that the parser reads none of them on a link.
@@ -938,12 +938,22 @@ export interface RigClippingAttachment extends RigVertexGeometry {
938
938
  * six then straddle the knots, and the constraint slides bones along a curve
939
939
  * nobody drew.
940
940
  *
941
- * ⚠️ `lengths` is NOT authored here. It is the cumulative length at the end of
942
- * each curve in the SETUP pose, in world units — a measurement of the geometry
943
- * above, and the same relationship `image` has to `width`/`height`: a restated
944
- * number can disagree with the vertices, and when it does, a
945
- * `constantSpeed: false` path traverses a length that is not the length of the
946
- * curve, silently. So rigc measures it and refuses an authored one by name.
941
+ * ⭐ `lengths` is **stated or measured** (issue #804). It is the cumulative
942
+ * length at the end of each curve, in world units, and `vertexCount / 3` entries
943
+ * on an open path and a closed one alike — the parser's allocation, one more
944
+ * than an open path's curves, the last being the wrap-around curve's cumulative,
945
+ * which nothing reads. Stated, it is emitted as stated: that is what `ingest`
946
+ * writes from an export, because the editor measured it on a pose rigc does not
947
+ * reproduce (below). Left out, rigc measures it off the geometry on the
948
+ * unconstrained setup pose. Only `constantSpeed: false` reads it.
949
+ *
950
+ * ⚠️ Why a stated one is not re-measured: the editor's numbers are
951
+ * `PathConstraint`'s own measurement of the pose the first update gives it —
952
+ * with every constraint ordered before the path constraint applied. rigc does
953
+ * not pose, so an authored path whose bones a constraint moves at rest gets the
954
+ * unconstrained figure, and an export's own array is the only way to carry the
955
+ * constrained one. Re-deriving it moved 232 of a production rig's 259 bones in
956
+ * issue #804's pose comparison.
947
957
  *
948
958
  * 🔸 *Which* length, exactly, is `SpinePathAttachment`'s subject in
949
959
  * [`types.ts`](types.ts) and it is not the arc: it is `PathConstraint`'s own
@@ -966,8 +976,8 @@ export interface RigPathAttachment extends RigVertexGeometry {
966
976
  */
967
977
  constantSpeed?: boolean;
968
978
  /**
969
- * 🚫 Refused by name. rigc measures the arc lengths off `vertices`/`weights` —
970
- * see the note above. The field is declared so the refusal can name it.
979
+ * Stated: exactly `vertexCount / 3` finite entries, none below the one before
980
+ * it, emitted as stated. Absent: measured — see the note above.
971
981
  */
972
982
  lengths?: number[];
973
983
  }
@@ -1631,7 +1641,11 @@ export interface RigSpec {
1631
1641
  bones: RigBone[];
1632
1642
  slots: RigSlot[];
1633
1643
  /**
1634
- * At least `default`, which becomes `skeletonData.defaultSkin` (`:441`).
1644
+ * `default`, if the rig has one, becomes `skeletonData.defaultSkin` (`:441`).
1645
+ * It is emitted exactly when this map carries the key — an empty `{}` included
1646
+ * — or a manifest part files its states under it; a rig whose art is all in
1647
+ * named skins states no `default` and gets none, which is what the editor's
1648
+ * export of such a rig declares (issue #801).
1635
1649
  *
1636
1650
  * Each entry is either the short form — `slotName -> placeholderName ->
1637
1651
  * attachment` — or the long one, `{ "attachments": {…}, "bones": [...],