Skip to content

Commit 47dfd70

Browse files
committed
Mark hand-contact movements as in development
1 parent 695678f commit 47dfd70

6 files changed

Lines changed: 49 additions & 73 deletions

File tree

packages/posecode-render/src/contacts.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -245,34 +245,6 @@ export function swingArms(
245245
if (changed) m.root.updateMatrixWorld(true);
246246
}
247247

248-
const GRIP_FRAME = new THREE.Quaternion().setFromAxisAngle(
249-
new THREE.Vector3(0, 1, 0),
250-
Math.PI,
251-
);
252-
253-
/**
254-
* Give gripping wrists a complete overhand contact frame. Arm IK only
255-
* constrains wrist position, leaving hand orientation underdetermined; without
256-
* this pass the fingers inherit the forearm direction and can point above the
257-
* bar. In root space the palm faces back (-Z), the fingers extend down (-Y),
258-
* and the wrist remains exactly on its solved anchor.
259-
*/
260-
export function orientBarGrips(m: Mannequin, grips: readonly GripTarget[]): void {
261-
let changed = false;
262-
for (const g of grips) {
263-
const side = /_(left|right)$/.exec(g.effector)?.[1];
264-
if (!side) continue;
265-
const wrist = m.bones.get(`wrist_${side}`);
266-
if (!wrist?.parent) continue;
267-
const rootWorld = m.root.getWorldQuaternion(new THREE.Quaternion());
268-
const desiredWorld = rootWorld.multiply(GRIP_FRAME);
269-
const parentWorld = wrist.parent.getWorldQuaternion(new THREE.Quaternion());
270-
wrist.quaternion.copy(parentWorld.invert().multiply(desiredWorld));
271-
changed = true;
272-
}
273-
if (changed) m.root.updateMatrixWorld(true);
274-
}
275-
276248
/** Max head turn toward a look target (radians) so the neck never over-rotates. */
277249
export const MAX_LOOK = 55 * (Math.PI / 180);
278250
const LOOK_FWD = new THREE.Vector3(0, 0, 1);

packages/posecode-render/src/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
type ClipSource,
2828
} from "./clips.js";
2929
import { depenetrate } from "./depenetrate.js";
30-
import { alignFloorPalms, levelPlantedFeet, wrapGrip, relaxHands, swingArms, aimHead, orientBarGrips } from "./contacts.js";
30+
import { alignFloorPalms, levelPlantedFeet, wrapGrip, relaxHands, swingArms, aimHead } from "./contacts.js";
3131

3232
const DEG = Math.PI / 180;
3333

@@ -549,9 +549,8 @@ export function createViewer(
549549
if (joints.length === 0) continue;
550550
solveCCD({ joints, limits, effector, target }, 12);
551551
}
552-
// 3. Resolve the wrist roll left underdetermined by positional arm IK.
553-
orientBarGrips(mannequin, grips);
554-
// 4. Finger wrap.
552+
// 3. Finger wrap. Wrist orientation remains inherited until the semantic
553+
// contact-frame solver can account for this character's calibrated axes.
555554
wrapGrip(mannequin, grips);
556555
}
557556

@@ -869,10 +868,13 @@ export function createViewer(
869868
if (lastIR) api.load(lastIR);
870869
else char.sync(mannequin);
871870
})
872-
.catch(() => {
871+
.catch((error: unknown) => {
873872
// Character failed (offline embed, blocked/404 CDN): reveal the
874873
// procedural figure we may have hidden, so the scene degrades to the
875-
// working fallback instead of staying blank. Deliberately silent.
874+
// working fallback instead of staying blank. Keep a developer-facing
875+
// diagnostic because malformed or incompatible rigs otherwise look
876+
// exactly like a network fallback and are impossible to calibrate.
877+
console.warn("Posecode character load failed; using procedural fallback", error);
876878
if (deferProceduralMeshes) setMeshVisibility(mannequin.root, true);
877879
});
878880
}

packages/posecode-render/test/contacts.test.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, expect } from "vitest";
22
import * as THREE from "three";
33
import { buildMannequin } from "../src/mannequin.js";
4-
import { levelPlantedFeet, relaxHands, swingArms, aimHead, orientBarGrips } from "../src/contacts.js";
4+
import { levelPlantedFeet, relaxHands, swingArms, aimHead } from "../src/contacts.js";
55
import { groundFigure } from "../src/groundlock.js";
66

77
const DEG = Math.PI / 180;
@@ -127,34 +127,6 @@ describe("swingArms (L4.2)", () => {
127127
});
128128
});
129129

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-
});
156-
});
157-
158130
describe("aimHead (L4.3 look-at)", () => {
159131
it("turns the head toward a focus point (face +Z tracks the target)", () => {
160132
const m = buildMannequin();

playground/src/main.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,18 @@ function renderLibraryList(): void {
337337
if (p.id === currentPresetId) item.setAttribute("aria-current", "true");
338338
const name = document.createElement("span");
339339
name.className = "li-name";
340-
name.textContent = p.label;
340+
const label = document.createElement("span");
341+
label.textContent = p.label;
342+
name.append(label);
343+
if (p.status === "development") {
344+
const status = document.createElement("span");
345+
status.className = "li-status";
346+
status.textContent = "In development";
347+
name.append(status);
348+
}
341349
const meta = document.createElement("span");
342350
meta.className = "li-meta";
343-
meta.textContent = `${p.target} · ${p.equipment} · ${p.difficulty}`;
351+
meta.textContent = p.developmentNote ?? `${p.target} · ${p.equipment} · ${p.difficulty}`;
344352
item.append(name, meta);
345353
item.addEventListener("click", () => {
346354
loadPreset(p.id);

playground/src/presets.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ export interface Preset {
112112
/** What it renders with: Body weight, Chair, Wall, or Bar. */
113113
equipment: string;
114114
difficulty: Difficulty;
115+
/** Optional product-readiness marker shown in the movement library. */
116+
status?: "development";
117+
/** Short user-facing explanation of the unfinished animation area. */
118+
developmentNote?: string;
115119
source: string;
116120
}
117121

@@ -138,9 +142,9 @@ export const PRESETS: Preset[] = [
138142
{ id: "calf-raise", label: "Single-leg calf raise", domain: "Fitness", bodyPart: "Lower legs", target: "Calves", equipment: "Body weight", difficulty: "Intermediate", source: calfRaise },
139143
{ id: "jumping-jacks", label: "Jumping jacks", domain: "Warm-up", bodyPart: "Full body", target: "Full body", equipment: "Body weight", difficulty: "Beginner", source: jumpingJacks },
140144
{ id: "box-step-taps", label: "Box step taps", domain: "Warm-up", bodyPart: "Upper legs", target: "Hip flexors", equipment: "Box", difficulty: "Beginner", source: boxStepTaps },
141-
{ id: "pull-up", label: "Pull-up", domain: "Fitness", bodyPart: "Back", target: "Lats", equipment: "Bar", difficulty: "Advanced", source: pullUp },
145+
{ id: "pull-up", label: "Pull-up", domain: "Fitness", bodyPart: "Back", target: "Lats", equipment: "Bar", difficulty: "Advanced", status: "development", developmentNote: "Hand grip and wrist contact are still being refined", source: pullUp },
142146
{ id: "step-up", label: "Step-up (box)", domain: "Functional", bodyPart: "Upper legs", target: "Quadriceps", equipment: "Box", difficulty: "Intermediate", source: stepUp },
143-
{ id: "triceps-dips", label: "Triceps dips (chair)", domain: "Fitness", bodyPart: "Upper arms", target: "Triceps", equipment: "Chair", difficulty: "Intermediate", source: tricepsDips },
147+
{ id: "triceps-dips", label: "Triceps dips (chair)", domain: "Fitness", bodyPart: "Upper arms", target: "Triceps", equipment: "Chair", difficulty: "Intermediate", status: "development", developmentNote: "Hand support and wrist contact are still being refined", source: tricepsDips },
144148
{ id: "quad-stretch", label: "Standing quad stretch", domain: "Mobility", bodyPart: "Upper legs", target: "Quadriceps", equipment: "Body weight", difficulty: "Beginner", source: quadStretch },
145149

146150
// --- Education / anatomy: single-joint ROM demos ---
@@ -198,14 +202,14 @@ export const PRESETS: Preset[] = [
198202
{ id: "sit-to-stand", label: "Sit to stand (chair)", domain: "Functional", bodyPart: "Upper legs", target: "Quadriceps", equipment: "Chair", difficulty: "Beginner", source: sitToStand },
199203
{ id: "box-squat", label: "Box squat (chair)", domain: "Fitness", bodyPart: "Upper legs", target: "Quadriceps", equipment: "Chair", difficulty: "Beginner", source: boxSquat },
200204
{ id: "wall-sit", label: "Wall sit (wall)", domain: "Fitness", bodyPart: "Upper legs", target: "Quadriceps", equipment: "Wall", difficulty: "Beginner", source: wallSit },
201-
{ id: "dead-hang", label: "Dead hang (bar)", domain: "Fitness", bodyPart: "Back", target: "Lats", equipment: "Bar", difficulty: "Beginner", source: deadHang },
202-
{ id: "hanging-knee-raise", label: "Hanging knee raise (bar)", domain: "Fitness", bodyPart: "Core", target: "Abdominals", equipment: "Bar", difficulty: "Intermediate", source: hangingKneeRaise },
205+
{ id: "dead-hang", label: "Dead hang (bar)", domain: "Fitness", bodyPart: "Back", target: "Lats", equipment: "Bar", difficulty: "Beginner", status: "development", developmentNote: "Hand grip and wrist contact are still being refined", source: deadHang },
206+
{ id: "hanging-knee-raise", label: "Hanging knee raise (bar)", domain: "Fitness", bodyPart: "Core", target: "Abdominals", equipment: "Bar", difficulty: "Intermediate", status: "development", developmentNote: "Hand grip and wrist contact are still being refined", source: hangingKneeRaise },
203207

204208
// --- Hand / finger rig ---
205-
{ id: "make-a-fist", label: "Make a fist", domain: "Hand therapy", bodyPart: "Hands", target: "Forearms", equipment: "Body weight", difficulty: "Beginner", source: makeAFist },
206-
{ id: "pinch-grip", label: "Pinch grip", domain: "Hand therapy", bodyPart: "Hands", target: "Forearms", equipment: "Body weight", difficulty: "Beginner", source: pinchGrip },
207-
{ id: "finger-spell", label: "Finger-spelling (approx.)", domain: "Sign language", bodyPart: "Hands", target: "Forearms", equipment: "Body weight", difficulty: "Beginner", source: fingerSpell },
208-
{ id: "hand-wave", label: "Hand wave", domain: "Sign language", bodyPart: "Hands", target: "Forearms", equipment: "Body weight", difficulty: "Beginner", source: handWave },
209+
{ id: "make-a-fist", label: "Make a fist", domain: "Hand therapy", bodyPart: "Hands", target: "Forearms", equipment: "Body weight", difficulty: "Beginner", status: "development", developmentNote: "Finger articulation is still being refined", source: makeAFist },
210+
{ id: "pinch-grip", label: "Pinch grip", domain: "Hand therapy", bodyPart: "Hands", target: "Forearms", equipment: "Body weight", difficulty: "Beginner", status: "development", developmentNote: "Finger articulation is still being refined", source: pinchGrip },
211+
{ id: "finger-spell", label: "Finger-spelling (approx.)", domain: "Sign language", bodyPart: "Hands", target: "Forearms", equipment: "Body weight", difficulty: "Beginner", status: "development", developmentNote: "Finger articulation is still being refined", source: fingerSpell },
212+
{ id: "hand-wave", label: "Hand wave", domain: "Sign language", bodyPart: "Hands", target: "Forearms", equipment: "Body weight", difficulty: "Beginner", status: "development", developmentNote: "Wrist and finger articulation are still being refined", source: handWave },
209213
{ id: "pirouette", label: "Pirouette (full turn)", domain: "Dance", bodyPart: "Full body", target: "Full body", equipment: "Body weight", difficulty: "Intermediate", source: pirouette },
210214
{ id: "box-step", label: "Box step (travels)", domain: "Dance", bodyPart: "Full body", target: "Full body", equipment: "Body weight", difficulty: "Beginner", source: boxStep },
211215
{ id: "grapevine", label: "Grapevine (travels)", domain: "Dance", bodyPart: "Full body", target: "Full body", equipment: "Body weight", difficulty: "Beginner", source: grapevine },

playground/src/style.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,11 +1119,29 @@ select:hover {
11191119
box-shadow: inset 2px 0 0 0 var(--accent);
11201120
}
11211121
.lib-item .li-name {
1122+
display: flex;
1123+
align-items: center;
1124+
gap: 7px;
1125+
flex-wrap: wrap;
11221126
font-family: var(--sans);
11231127
font-size: 13.5px;
11241128
font-weight: 600;
11251129
color: var(--text);
11261130
}
1131+
.lib-item .li-status {
1132+
flex: none;
1133+
font-family: var(--mono);
1134+
font-size: 9px;
1135+
font-weight: 700;
1136+
line-height: 1;
1137+
letter-spacing: 0.45px;
1138+
text-transform: uppercase;
1139+
color: #f0bd68;
1140+
background: rgba(240, 189, 104, 0.1);
1141+
border: 1px solid rgba(240, 189, 104, 0.3);
1142+
border-radius: 999px;
1143+
padding: 4px 6px 3px;
1144+
}
11271145
.lib-item .li-meta {
11281146
font-size: 11.5px;
11291147
color: var(--muted);

0 commit comments

Comments
 (0)