@flighthq/skeleton2d 0.3.1-next.847.2e6f8bf → 0.3.1-next.861.5a56d7a
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 +10 -10
- package/src/ikConstraint2D.test.ts +58 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/skeleton2d",
|
|
3
|
-
"version": "0.3.1-next.
|
|
3
|
+
"version": "0.3.1-next.861.5a56d7a",
|
|
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.
|
|
42
|
-
"@flighthq/entity": "0.3.1-next.
|
|
43
|
-
"@flighthq/geometry": "0.3.1-next.
|
|
44
|
-
"@flighthq/log": "0.3.1-next.
|
|
45
|
-
"@flighthq/node": "0.3.1-next.
|
|
46
|
-
"@flighthq/math": "0.3.1-next.
|
|
47
|
-
"@flighthq/path": "0.3.1-next.
|
|
48
|
-
"@flighthq/registry": "0.3.1-next.
|
|
49
|
-
"@flighthq/types": "0.3.1-next.
|
|
41
|
+
"@flighthq/animation": "0.3.1-next.861.5a56d7a",
|
|
42
|
+
"@flighthq/entity": "0.3.1-next.861.5a56d7a",
|
|
43
|
+
"@flighthq/geometry": "0.3.1-next.861.5a56d7a",
|
|
44
|
+
"@flighthq/log": "0.3.1-next.861.5a56d7a",
|
|
45
|
+
"@flighthq/node": "0.3.1-next.861.5a56d7a",
|
|
46
|
+
"@flighthq/math": "0.3.1-next.861.5a56d7a",
|
|
47
|
+
"@flighthq/path": "0.3.1-next.861.5a56d7a",
|
|
48
|
+
"@flighthq/registry": "0.3.1-next.861.5a56d7a",
|
|
49
|
+
"@flighthq/types": "0.3.1-next.861.5a56d7a"
|
|
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 }),
|