@flighthq/skeleton2d 0.3.1-next.847.2e6f8bf → 0.3.1-next.887.9f59a84

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": "@flighthq/skeleton2d",
3
- "version": "0.3.1-next.847.2e6f8bf",
3
+ "version": "0.3.1-next.887.9f59a84",
4
4
  "author": "Joshua Granick and other contributors",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,15 +38,15 @@
38
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@flighthq/animation": "0.3.1-next.847.2e6f8bf",
42
- "@flighthq/entity": "0.3.1-next.847.2e6f8bf",
43
- "@flighthq/geometry": "0.3.1-next.847.2e6f8bf",
44
- "@flighthq/log": "0.3.1-next.847.2e6f8bf",
45
- "@flighthq/node": "0.3.1-next.847.2e6f8bf",
46
- "@flighthq/math": "0.3.1-next.847.2e6f8bf",
47
- "@flighthq/path": "0.3.1-next.847.2e6f8bf",
48
- "@flighthq/registry": "0.3.1-next.847.2e6f8bf",
49
- "@flighthq/types": "0.3.1-next.847.2e6f8bf"
41
+ "@flighthq/animation": "0.3.1-next.887.9f59a84",
42
+ "@flighthq/entity": "0.3.1-next.887.9f59a84",
43
+ "@flighthq/geometry": "0.3.1-next.887.9f59a84",
44
+ "@flighthq/log": "0.3.1-next.887.9f59a84",
45
+ "@flighthq/node": "0.3.1-next.887.9f59a84",
46
+ "@flighthq/math": "0.3.1-next.887.9f59a84",
47
+ "@flighthq/path": "0.3.1-next.887.9f59a84",
48
+ "@flighthq/registry": "0.3.1-next.887.9f59a84",
49
+ "@flighthq/types": "0.3.1-next.887.9f59a84"
50
50
  },
51
51
  "devDependencies": {
52
52
  "typescript": "^5.3.0"
@@ -284,6 +284,64 @@ describe('solveSkeleton2DIkConstraint', () => {
284
284
  expect(skeleton.bones[0].scaleX).toBeCloseTo(2, 5);
285
285
  });
286
286
 
287
+ // ★ THE PARAMETER SPACE, AS A STANDING PROPERTY RATHER THAN A NUMBER IN A REPORT.
288
+ //
289
+ // When the dead-zone branch was removed as redundant, the evidence was a sweep of 1,536 parameter
290
+ // combinations showing byte-identical output with and without it. That sweep lived in a scratch probe
291
+ // and was deleted with it, which left the removal resting on a figure in a message — integration
292
+ // flagged that it was the ONLY evidence, since no committed test enumerated the space. This is that
293
+ // evidence, made durable and turned the right way round: it does not assert what the old branch did,
294
+ // it asserts what the SOLVER MUST DO, so it constrains every future edit rather than one past one.
295
+ //
296
+ // The property is the definition of a two-bone solve, and it has exactly three cases with no gaps
297
+ // between them. For a target at distance d from the parent's origin, with the chain fully mixed:
298
+ // d > pL + cL out of reach — the chain straightens, the tip sits at pL + cL along the aim
299
+ // d < |pL - cL| dead zone — the chain folds, the tip sits at |pL - cL| along the aim
300
+ // otherwise reachable — the tip lands ON the target
301
+ // Anything that breaks a clamp, an angle, or the branch structure lands outside one of the three.
302
+ it('places the tip correctly across the whole reachable/unreachable/dead-zone space', () => {
303
+ const lengths = [10, 6, 3, 12.5];
304
+ const targets = [-8, -2, -0.5, 0.5, 2, 5, 9, 30];
305
+ let checked = 0;
306
+
307
+ for (const parentLength of lengths) {
308
+ for (const childLength of lengths) {
309
+ for (const tx of targets) {
310
+ for (const ty of [0, -3, 4.5]) {
311
+ for (const bendPositive of [true, false]) {
312
+ const skeleton = createSkeleton2D([
313
+ makeBone({ length: parentLength }),
314
+ makeBone({ length: childLength, parentIndex: 0, x: parentLength }),
315
+ makeBone({ x: tx, y: ty }),
316
+ ]);
317
+ computeSkeleton2DWorldTransforms(skeleton);
318
+
319
+ solveSkeleton2DIkConstraint(skeleton, ik({ bendPositive, boneIndices: [0, 1], targetBoneIndex: 2 }));
320
+ computeSkeleton2DWorldTransforms(skeleton);
321
+
322
+ const reach = Math.hypot(tx, ty);
323
+ if (reach === 0) continue; // no aim direction exists; the solver documents this as skipped
324
+ const span = parentLength + childLength;
325
+ const fold = Math.abs(parentLength - childLength);
326
+ const wanted = reach > span ? span : reach < fold ? fold : reach;
327
+ // Whatever the case, the tip lies at `wanted` along the direction of the target. Compared as
328
+ // a POSITION rather than a length plus an angle: atan2 reports -pi and +pi for the same
329
+ // direction, so an angle comparison fails on a target along -x for a reason that is about
330
+ // the assertion rather than the solver.
331
+ const tip = tipOf(skeleton, 1);
332
+ const where = `p=${parentLength} c=${childLength} t=(${tx},${ty}) bend=${bendPositive}`;
333
+ expect(tip.x, `${where} tip.x`).toBeCloseTo((wanted * tx) / reach, 4);
334
+ expect(tip.y, `${where} tip.y`).toBeCloseTo((wanted * ty) / reach, 4);
335
+ checked++;
336
+ }
337
+ }
338
+ }
339
+ }
340
+ }
341
+ // A sweep that silently stopped enumerating would pass every assertion above.
342
+ expect(checked).toBeGreaterThan(700);
343
+ });
344
+
287
345
  it('skips a chain length it does not solve rather than posing something wrong', () => {
288
346
  const skeleton = createSkeleton2D([
289
347
  makeBone({ length: 10 }),