|
1 | 1 | import { describe, it, expect } from "vitest"; |
2 | 2 | import * as THREE from "three"; |
3 | 3 | import { buildMannequin } from "../src/mannequin.js"; |
4 | | -import { levelPlantedFeet, relaxHands, swingArms, aimHead } from "../src/contacts.js"; |
| 4 | +import { levelPlantedFeet, relaxHands, swingArms, aimHead, orientBarGrips } from "../src/contacts.js"; |
5 | 5 | import { groundFigure } from "../src/groundlock.js"; |
6 | 6 |
|
7 | 7 | const DEG = Math.PI / 180; |
@@ -104,6 +104,43 @@ describe("swingArms (L4.2)", () => { |
104 | 104 | swingArms(m, new Set(), new Set(["left"])); |
105 | 105 | expect(m.bones.get("shoulder_left")!.rotation.x).toBeCloseTo(before, 5); |
106 | 106 | }); |
| 107 | + |
| 108 | + it("is idempotent across render frames instead of accumulating a spin", () => { |
| 109 | + const m = buildMannequin(); |
| 110 | + m.bones.get("hip_right")!.rotation.x = -0.6; |
| 111 | + swingArms(m, new Set(), new Set()); |
| 112 | + const once = m.bones.get("shoulder_left")!.quaternion.clone(); |
| 113 | + for (let frame = 0; frame < 120; frame++) swingArms(m, new Set(), new Set()); |
| 114 | + expect(m.bones.get("shoulder_left")!.quaternion.angleTo(once)).toBeLessThan(1e-6); |
| 115 | + }); |
| 116 | +}); |
| 117 | + |
| 118 | +describe("orientBarGrips", () => { |
| 119 | + it("resolves wrist roll without moving a hand off its grip point", () => { |
| 120 | + const m = buildMannequin(); |
| 121 | + const wrist = m.bones.get("wrist_left")!; |
| 122 | + wrist.rotation.y = 1.7; |
| 123 | + m.root.updateMatrixWorld(true); |
| 124 | + const position = wrist.getWorldPosition(new THREE.Vector3()).clone(); |
| 125 | + orientBarGrips(m, [{ effector: "hand_left", anchor: "bar_left" }]); |
| 126 | + expect(wrist.getWorldPosition(new THREE.Vector3()).distanceTo(position)).toBeLessThan(1e-6); |
| 127 | + |
| 128 | + const palm = new THREE.Vector3(0, 0, 1) |
| 129 | + .applyQuaternion(wrist.getWorldQuaternion(new THREE.Quaternion())); |
| 130 | + const fingers = new THREE.Vector3(0, -1, 0) |
| 131 | + .applyQuaternion(wrist.getWorldQuaternion(new THREE.Quaternion())); |
| 132 | + expect(palm.dot(new THREE.Vector3(0, 0, -1))).toBeGreaterThan(0.999); |
| 133 | + expect(fingers.dot(new THREE.Vector3(0, -1, 0))).toBeGreaterThan(0.999); |
| 134 | + }); |
| 135 | + |
| 136 | + it("is stable when applied repeatedly", () => { |
| 137 | + const m = buildMannequin(); |
| 138 | + const grips = [{ effector: "hand_right", anchor: "bar_right" }]; |
| 139 | + orientBarGrips(m, grips); |
| 140 | + const once = m.bones.get("wrist_right")!.quaternion.clone(); |
| 141 | + for (let frame = 0; frame < 60; frame++) orientBarGrips(m, grips); |
| 142 | + expect(m.bones.get("wrist_right")!.quaternion.angleTo(once)).toBeLessThan(1e-6); |
| 143 | + }); |
107 | 144 | }); |
108 | 145 |
|
109 | 146 | describe("aimHead (L4.3 look-at)", () => { |
|
0 commit comments