@rig-c/core 2.58.1 → 2.60.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/stop.ts CHANGED
@@ -119,6 +119,13 @@ const TWIN_FIELDS = ['length', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX
119
119
  /** A degree bound a clamp can reach: a local rotation read off a world copy is in (-180, 180]. */
120
120
  const BOUND = 180;
121
121
 
122
+ /** What the first link's bound refusal names: the link's hang and root, and the point — world, at the setup pose. */
123
+ interface FirstLink {
124
+ hang: number;
125
+ root: readonly [number, number];
126
+ point: readonly [number, number];
127
+ }
128
+
122
129
  /** An angle in (-540, 540) brought into (-180, 180] by at most one whole turn. */
123
130
  function halfTurn(deg: number): number {
124
131
  if (deg > 180) return deg - 360;
@@ -332,13 +339,36 @@ export function expandStops(spec: RigSpec, where: string, plant: StopPlant = {})
332
339
  properties: { rotate: { offset: lo, to: { rotate: { offset: lo, max: hi, scale: 1 } } } },
333
340
  mixRotate: 1,
334
341
  });
335
- const bounds = (stop: ReadStop, rest: number, sign: number, range: { inward: number; outward: number }, field: string, what: string): [number, number] => {
342
+ /**
343
+ * The first link's half of a bound refusal (issue #1455): where its rest
344
+ * angle came from, and what to move. Every figure is one the expansion
345
+ * already read — the link's root and hang and the point, in the exact frame's
346
+ * world (Spine world, y up) at the setup pose — and the bound is
347
+ * `|rest| + end`, since inward's sign is the opposite of rest's.
348
+ */
349
+ const aimed = (stop: ReadStop, rest: number, end: 'inward' | 'outward', at: FirstLink): string => {
350
+ const e = stop[end];
351
+ const off = Math.abs(rest);
352
+ const sum = off + e;
353
+ const geometry =
354
+ `. "${stop.links[0]}" hangs at ${at.hang}° from its root at [${at.root.join(', ')}], and "${stop.names.point}" ("at" [${stop.at.join(', ')}] on "${stop.against}") ` +
355
+ `sits at [${at.point.join(', ')}] — Spine world, y up, at the setup pose — so the aim from that root to the point lies ${off}° off the hang (rest ${rest}°): ` +
356
+ `|rest| ${off}° + "${end}" ${e}° is ${sum}°`;
357
+ if (sum < -BOUND) return `${geometry}, below -${BOUND}°. State "${end}" at least ${-BOUND - off}°`;
358
+ if (e >= BOUND) return `${geometry}, above ${BOUND}°, and no point lowers it: |rest| is above 0° for every point a stop accepts. State "${end}" below ${BOUND}°`;
359
+ return (
360
+ `${geometry}, above ${BOUND}°. Move "${stop.names.point}" to within ${BOUND - e}° (${BOUND}° − "${end}") of the direction "${stop.links[0]}" hangs in from its root, ${at.hang}°, ` +
361
+ `or state "${end}" at most ${BOUND - off}°`
362
+ );
363
+ };
364
+ const bounds = (stop: ReadStop, rest: number, sign: number, range: { inward: number; outward: number }, field: string, what: string, first?: FirstLink): [number, number] => {
336
365
  const a = rest - sign * range.outward;
337
366
  const b = rest - sign * range.inward;
338
367
  for (const [end, value] of [['outward', a], ['inward', b]] as const) {
339
368
  if (Math.abs(value) > BOUND) {
369
+ const why = first === undefined ? '' : aimed(stop, rest, end, first);
340
370
  throw new CompileError(
341
- `${stop.who}: "${field}${end}" ${range[end]} puts ${what}'s bound at ${value}°, past ±${BOUND}° — the clamp reads a local rotation in (-180, 180], so a bound past it is never reached and the range it states is not the one that holds`, 'CE_STOP_LOCAL_BOUND_RANGE',
371
+ `${stop.who}: "${field}${end}" ${range[end]} puts ${what}'s bound at ${value}°, past ±${BOUND}° — the clamp reads a local rotation in (-180, 180], so a bound past it is never reached and the range it states is not the one that holds${why}`, 'CE_STOP_LOCAL_BOUND_RANGE',
342
372
  );
343
373
  }
344
374
  }
@@ -407,7 +437,7 @@ export function expandStops(spec: RigSpec, where: string, plant: StopPlant = {})
407
437
  // Inward turns the link toward the aim line: counter-clockwise when the link
408
438
  // lies clockwise of it (a negative rest angle), clockwise otherwise.
409
439
  const sign = (rest < 0 ? 1 : -1) * (plant.swapRange ? -1 : 1);
410
- const [lo, hi] = bounds(stop, rest, sign, stop, '', `"${names.drawn[0]}"`);
440
+ const [lo, hi] = bounds(stop, rest, sign, stop, '', `"${names.drawn[0]}"`, { hang: halfTurn(linkFrame.worldRotation), root: [linkFrame.worldX, linkFrame.worldY], point });
411
441
 
412
442
  const frame: RigBone = { name: names.frame, parent };
413
443
  if (link.x !== undefined) frame.x = link.x;