spine-rigc 2.32.1 → 2.32.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "2.32.1",
3
+ "version": "2.32.2",
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": {
@@ -30,12 +30,27 @@ export interface Carrier {
30
30
  bary: [number, number, number];
31
31
  }
32
32
 
33
+ /**
34
+ * Issue #1323's faults in resolving several carriers, for the `mesh-compare` suite's controls: the rule before the
35
+ * issue (the per-coordinate support alone, `slackSupport` never read); the slack read as the narrowest triangle's
36
+ * rather than the widest's; and any shared vertex excused whatever the distance, so two triangles that both hold a
37
+ * sample well inside — an overlap — carry it by the first.
38
+ */
39
+ export type CarrierPlant = 'slack-support-unread' | 'narrowest-slack' | 'any-shared-vertex';
40
+
33
41
  /**
34
42
  * Each sample's carrier in one mesh's UV triangulation — §3 and correction 4.
35
43
  * Throws `COMPARE_UV_CARRIER_NOT_UNIQUE` for a sample two triangles carry
36
- * other than across a vertex or edge they share.
44
+ * other than across a vertex or edge they share — where "across" is read, since
45
+ * issue #1323, to the distance the containment slack itself allows
46
+ * (`slackSupport`).
37
47
  */
38
48
  export function uvCarriers(uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> {
49
+ return uvCarriersWith(uvs, triangles, samples, who, null);
50
+ }
51
+
52
+ /** `uvCarriers` with a fault planted in resolving several carriers (`CarrierPlant`). Internal: on no entry. */
53
+ export function uvCarriersWith(uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string, plant: CarrierPlant | null): Array<Carrier | null> {
39
54
  const areas = triangleAreas(uvs, triangles);
40
55
  const band = areaBand(areas, uvs);
41
56
  const live: number[] = [];
@@ -101,7 +116,8 @@ export function uvCarriers(uvs: readonly number[], triangles: readonly number[],
101
116
  }
102
117
  });
103
118
  }
104
- return samples.map((s, j) => resolveCarriers(s.uv[0], s.uv[1], hits[j] ?? [], who));
119
+ const uvOf = (v: number): readonly [number, number] => [uvs[v * 2], uvs[v * 2 + 1]];
120
+ return samples.map((s, j) => resolveCarriers(s.uv[0], s.uv[1], hits[j] ?? [], who, uvOf, plant));
105
121
  }
106
122
 
107
123
  /**
@@ -143,21 +159,32 @@ export function carrierIn(
143
159
 
144
160
  /**
145
161
  * The carrier of a sample from every triangle that contains it: none, the one, or — several — the first when they all
146
- * meet there at a shared vertex or edge; else `COMPARE_UV_CARRIER_NOT_UNIQUE` (correction 4).
162
+ * meet there at a shared vertex or edge; else `COMPARE_UV_CARRIER_NOT_UNIQUE` (correction 4). Where the per-coordinate
163
+ * support refuses, the support is read again to the slack's distance (issue #1323, {@link slackSupport}) when the
164
+ * corners' UVs are given (`uvOf`).
147
165
  */
148
- export function resolveCarriers(px: number, py: number, hits: readonly Carrier[], who: string): Carrier | null {
166
+ export function resolveCarriers(
167
+ px: number,
168
+ py: number,
169
+ hits: readonly Carrier[],
170
+ who: string,
171
+ uvOf: ((v: number) => readonly [number, number]) | null = null,
172
+ plant: CarrierPlant | null = null,
173
+ ): Carrier | null {
149
174
  if (hits.length === 0) return null;
150
175
  if (hits.length === 1) return hits[0];
151
176
  // Several carriers are one hit only across a vertex or an edge they all share: the sample's support — the corners
152
177
  // it does not sit at zero weight on — is then the same vertex indices in each, and the carried point is theirs.
153
- const support = (c: Carrier): string =>
178
+ const shared = (supportOf: (c: Carrier) => string): boolean => {
179
+ const first = supportOf(hits[0]);
180
+ return first.split(',').length <= 2 && hits.every((c) => supportOf(c) === first);
181
+ };
182
+ const byCoordinate = (c: Carrier): string =>
154
183
  c.corners
155
184
  .filter((_v, k) => c.bary[k] > CONTAINS)
156
185
  .sort((a, b) => a - b)
157
186
  .join(',');
158
- const first = support(hits[0]);
159
- const shared = first.split(',').length <= 2 && hits.every((c) => support(c) === first);
160
- if (!shared) {
187
+ if (!shared(byCoordinate) && (uvOf === null || plant === 'slack-support-unread' || !shared(slackSupport(hits, uvOf, plant)))) {
161
188
  refuse(
162
189
  'COMPARE_UV_CARRIER_NOT_UNIQUE',
163
190
  `${who}: the sample at uv (${px}, ${py}) lies in ${hits.length} UV triangles — ${hits.map((c) => `triangle ${c.triangle} (vertices ${c.corners.join(', ')})`).join(' and ')} — that do not meet there at a shared vertex or edge; required one carrier per sample (correction 4: overlapping or folded UV triangles would carry it by an arbitrary one)`,
@@ -165,3 +192,48 @@ export function resolveCarriers(px: number, py: number, hits: readonly Carrier[]
165
192
  }
166
193
  return hits[0];
167
194
  }
195
+
196
+ /**
197
+ * Issue #1323: a carrier's support read to the slack's distance rather than per coordinate — the corners the sample
198
+ * lies further than `CONTAINS × H` (UV units) inside of, measured from each corner's opposite edge, with `H` the widest
199
+ * triangle height among the hits: the furthest outside a triangle the containment test (`carrierIn`) admitted any of
200
+ * them at this sample.
201
+ *
202
+ * Why: the slack is per barycentric coordinate, so in distance it is `CONTAINS` times the height of the corner it is
203
+ * read against. A sample on a shared edge, a hair to one side — a pixel centre on a 45° or axis-parallel edge between
204
+ * whole-pixel vertices, put off it by the UVs' rounding — is admitted by the triangle it is outside of, while the
205
+ * triangle it is inside of, if it is a sliver, reads its third corner's coordinate above `CONTAINS` because its height
206
+ * is small: two per-coordinate supports, read as an overlap though the sample is within the slack of the edge both
207
+ * share. Measured on demo/bottomwear's reduction at source spacing 18: 1.35e-10 uv from the edge, coordinates −3.2e-10
208
+ * and +6.5e-8, heights 0.42 and 0.0021. Read in distance, both corners are within the slack and the support is the
209
+ * shared edge. A sample two triangles both hold further inside than that — an overlap or a fold — is still refused.
210
+ */
211
+ function slackSupport(hits: readonly Carrier[], uvOf: (v: number) => readonly [number, number], plant: CarrierPlant | null): (c: Carrier) => string {
212
+ if (plant === 'any-shared-vertex') {
213
+ const common = hits[0].corners.filter((v) => hits.every((c) => c.corners.includes(v)));
214
+ return () => (common.length > 0 ? [...common].sort((a, b) => a - b).slice(0, 1).join(',') : 'none');
215
+ }
216
+ const heights = new Map<Carrier, [number, number, number]>();
217
+ let widest = 0;
218
+ let narrowest = Infinity;
219
+ for (const c of hits) {
220
+ const p = c.corners.map(uvOf);
221
+ const twice = Math.abs((p[1][0] - p[0][0]) * (p[2][1] - p[0][1]) - (p[2][0] - p[0][0]) * (p[1][1] - p[0][1]));
222
+ const h = [0, 1, 2].map((k) => {
223
+ const a = p[(k + 1) % 3];
224
+ const b = p[(k + 2) % 3];
225
+ return twice / Math.hypot(b[0] - a[0], b[1] - a[1]);
226
+ }) as [number, number, number];
227
+ heights.set(c, h);
228
+ widest = Math.max(widest, ...h);
229
+ narrowest = Math.min(narrowest, ...h);
230
+ }
231
+ const slack = CONTAINS * (plant === 'narrowest-slack' ? narrowest : widest);
232
+ return (c: Carrier): string => {
233
+ const h = heights.get(c)!;
234
+ return c.corners
235
+ .filter((_v, k) => c.bary[k] * h[k] > slack)
236
+ .sort((a, b) => a - b)
237
+ .join(',');
238
+ };
239
+ }
@@ -104,7 +104,7 @@ import {
104
104
  type WorstSample,
105
105
  } from './meshquality.ts';
106
106
  import { areaBand, stretchSingularValues, triangleAreas } from './areaband.ts';
107
- import { uvCarriers, type Carrier } from './meshcarriers.ts';
107
+ import { uvCarriers, uvCarriersWith, type Carrier, type CarrierPlant } from './meshcarriers.ts';
108
108
  import { CoreInputError, readModel, underNoSkin, underSkin, type CompiledDocument } from './core/index.ts';
109
109
  import { poseRawAnimationEach, poseRawSetup, type RawPose } from './core/raw.ts';
110
110
  import { sampleTime } from './core/animation.ts';
@@ -186,7 +186,9 @@ export interface MotionComparisonInput {
186
186
  * Issue #1291's faults in carrying `motionAmplitude` into the setup measurement, for the `mesh-compare` suite's
187
187
  * negative controls: the amplitude not handed over; `null` handed over as the field left out; the field not validated
188
188
  * before the builds are read; `null` echoed for a field left out; and the load counted as a required row, so the
189
- * amplitude moves a verdict. `compareMeshesInMotion` plants none.
189
+ * amplitude moves a verdict. `compareMeshesInMotion` plants none. Issue #1323 adds `CarrierPlant`
190
+ * (`src/meshcarriers.ts`): a fault in resolving a sample's several carriers, planted in every `uvCarriers` the
191
+ * comparison reads.
190
192
  */
191
193
  export type ComparePlant =
192
194
  | 'amplitude-not-carried'
@@ -194,7 +196,8 @@ export type ComparePlant =
194
196
  | 'amplitude-unvalidated'
195
197
  | 'echo-when-unset'
196
198
  | 'amplitude-gates-acceptance'
197
- | OverBoundPlant;
199
+ | OverBoundPlant
200
+ | CarrierPlant;
198
201
 
199
202
  /**
200
203
  * Issue #1315's faults in `overBound`, for the `mesh-compare` suite's negative controls: a listed sample's triangle
@@ -995,11 +998,14 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
995
998
  }
996
999
  return { samples: out, art };
997
1000
  });
998
- const refCarriers = order.map((i, k) => uvCarriers(reference.meshes[i].uvs, reference.meshes[i].triangles, samplesOf[k].samples, `reference "${reference.id}", attachment ${nameOf(refs[i])}`));
1001
+ const carrierPlant: CarrierPlant | null = plant === 'slack-support-unread' || plant === 'narrowest-slack' || plant === 'any-shared-vertex' ? plant : null;
1002
+ const carriersOf = (uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> =>
1003
+ uvCarriersWith(uvs, triangles, samples, who, carrierPlant);
1004
+ const refCarriers = order.map((i, k) => carriersOf(reference.meshes[i].uvs, reference.meshes[i].triangles, samplesOf[k].samples, `reference "${reference.id}", attachment ${nameOf(refs[i])}`));
999
1005
  const posedRef = poseBuild(reference.doc, order.map((i) => refs[i]), frames, walks);
1000
1006
 
1001
1007
  motionOf = (b: Build, isReference: boolean) => {
1002
- const carriers = isReference ? refCarriers : order.map((i, k) => uvCarriers(b.meshes[i].uvs, b.meshes[i].triangles, samplesOf[k].samples, `candidate "${b.id}", attachment ${nameOf(refs[i])}`));
1008
+ const carriers = isReference ? refCarriers : order.map((i, k) => carriersOf(b.meshes[i].uvs, b.meshes[i].triangles, samplesOf[k].samples, `candidate "${b.id}", attachment ${nameOf(refs[i])}`));
1003
1009
  const posed = isReference ? posedRef : poseBuild(b.doc, order.map((i) => refs[i]), frames, walks);
1004
1010
  const built: Built[] = [];
1005
1011
  const perFrame: NonNullable<CandidateReport['perFrame']> = [];
@@ -1235,7 +1235,7 @@ export class SkinningCarry {
1235
1235
  }
1236
1236
  let carrier: Carrier | null;
1237
1237
  try {
1238
- carrier = resolveCarriers(this.su[j], this.sv[j], hits, `${this.who}: candidate (working vertex ids)`);
1238
+ carrier = resolveCarriers(this.su[j], this.sv[j], hits, `${this.who}: candidate (working vertex ids)`, (v) => this.mesh.uv[v]);
1239
1239
  } catch (err) {
1240
1240
  if (!(err instanceof MeshReductionError)) throw err;
1241
1241
  if (pending.refused === null) pending.refused = `SKINNING_UV_CARRIER_NOT_UNIQUE: ${err.message}`;