|
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; |
@@ -116,6 +116,43 @@ describe("swingArms (L4.2)", () => { |
116 | 116 | swingArms(m, new Set(), new Set(["left"])); |
117 | 117 | expect(m.bones.get("shoulder_left")!.rotation.x).toBeCloseTo(before, 5); |
118 | 118 | }); |
| 119 | + |
| 120 | + it("is idempotent across render frames instead of accumulating a spin", () => { |
| 121 | + const m = buildMannequin(); |
| 122 | + m.bones.get("hip_right")!.rotation.x = -0.6; |
| 123 | + swingArms(m, new Set(), new Set()); |
| 124 | + const once = m.bones.get("shoulder_left")!.quaternion.clone(); |
| 125 | + for (let frame = 0; frame < 120; frame++) swingArms(m, new Set(), new Set()); |
| 126 | + expect(m.bones.get("shoulder_left")!.quaternion.angleTo(once)).toBeLessThan(1e-6); |
| 127 | + }); |
| 128 | +}); |
| 129 | + |
| 130 | +describe("orientBarGrips", () => { |
| 131 | + it("resolves wrist roll without moving a hand off its grip point", () => { |
| 132 | + const m = buildMannequin(); |
| 133 | + const wrist = m.bones.get("wrist_left")!; |
| 134 | + wrist.rotation.y = 1.7; |
| 135 | + m.root.updateMatrixWorld(true); |
| 136 | + const position = wrist.getWorldPosition(new THREE.Vector3()).clone(); |
| 137 | + orientBarGrips(m, [{ effector: "hand_left", anchor: "bar_left" }]); |
| 138 | + expect(wrist.getWorldPosition(new THREE.Vector3()).distanceTo(position)).toBeLessThan(1e-6); |
| 139 | + |
| 140 | + const palm = new THREE.Vector3(0, 0, 1) |
| 141 | + .applyQuaternion(wrist.getWorldQuaternion(new THREE.Quaternion())); |
| 142 | + const fingers = new THREE.Vector3(0, -1, 0) |
| 143 | + .applyQuaternion(wrist.getWorldQuaternion(new THREE.Quaternion())); |
| 144 | + expect(palm.dot(new THREE.Vector3(0, 0, -1))).toBeGreaterThan(0.999); |
| 145 | + expect(fingers.dot(new THREE.Vector3(0, -1, 0))).toBeGreaterThan(0.999); |
| 146 | + }); |
| 147 | + |
| 148 | + it("is stable when applied repeatedly", () => { |
| 149 | + const m = buildMannequin(); |
| 150 | + const grips = [{ effector: "hand_right", anchor: "bar_right" }]; |
| 151 | + orientBarGrips(m, grips); |
| 152 | + const once = m.bones.get("wrist_right")!.quaternion.clone(); |
| 153 | + for (let frame = 0; frame < 60; frame++) orientBarGrips(m, grips); |
| 154 | + expect(m.bones.get("wrist_right")!.quaternion.angleTo(once)).toBeLessThan(1e-6); |
| 155 | + }); |
119 | 156 | }); |
120 | 157 |
|
121 | 158 | describe("aimHead (L4.3 look-at)", () => { |
|
0 commit comments