File size: 9,526 Bytes
970fc3e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | const HAND_SIDES = Object.freeze(["left", "right"]);
const half = Math.SQRT1_2;
const HAND_GRIP_BASIS = Object.freeze({ left: Object.freeze([half, 0, half, 0]), right: Object.freeze([half, 0, -half, 0]) });
const fittedRotation = (side, degrees) => {
const [x, y, z, w] = HAND_GRIP_BASIS[side], s = Math.sin(degrees * Math.PI / 360), c = Math.cos(degrees * Math.PI / 360);
return [c * x + s * w, c * y - s * z, c * z + s * y, c * w - s * x];
};
const HANDS_METADATA = Object.freeze({
version: 1,
modelId: "whitebox-controller-hands",
modelVersion: 5,
space: "content-world",
units: "meters",
poseConvention: "palm-after-grip-offset",
clock: "animation-frame-ms",
grasp: "controller-fingers",
calibration: "default-unmeasured",
appearance: Object.freeze({ color: "#58a6d8" }),
dimensions: Object.freeze({ palmWidth: 0.078, palmLength: 0.086, palmThickness: 0.03, wristLength: 0.04, fingerRadius: 8e-3 }),
fixedPose: Object.freeze({ grip: 0.65, trigger: 0.15 }),
gripToPalm: Object.freeze({
// WebXR: grip +Y points toward the arm, -Z toward the thumb, and
// the back of the hand faces -X (left) / +X (right). Model +Y is
// distal and +Z is palmar. The grip origin is inside the curled fist.
left: Object.freeze({ positionMeters: Object.freeze([-0.022, 0.012, 0.016]), quaternionXYZW: Object.freeze(fittedRotation("left", 45)) }),
right: Object.freeze({ positionMeters: Object.freeze([0.022, 0.012, 0.016]), quaternionXYZW: Object.freeze(fittedRotation("right", 45)) })
}),
armModel: Object.freeze({
version: 1,
solver: "two-bone-pole-1",
tracking: "head-and-grips-inferred-shoulders-elbows",
shoulderWidth: 0.38,
headToShoulderDrop: 0.24,
headToShoulderBack: 0.07,
upperArmLength: 0.29,
forearmLength: 0.26,
maxShoulderShift: 0.06,
upperArmRadius: 0.043,
forearmRadius: 0.033,
palmToWrist: Object.freeze([0, -0.062, 0]),
headYawDeadband: 0.96,
maxTorsoTurnSpeed: 0.65,
poleSmoothing: 8
})
});
const vector = (v, n) => Array.isArray(v) && v.length === n && v.every(Number.isFinite);
const keys = (o, allowed) => o && typeof o === "object" && !Array.isArray(o) && Object.keys(o).every((k) => allowed.includes(k));
const unit = (n) => Number.isFinite(n) && n >= 0 && n <= 1;
function validHandPose(p, maxPosition = 100) {
return keys(p, ["positionMeters", "quaternionXYZW"]) && vector(p.positionMeters, 3) && p.positionMeters.every((n) => Math.abs(n) <= maxPosition) && vector(p.quaternionXYZW, 4) && Math.abs(Math.hypot(...p.quaternionXYZW) - 1) < 1e-3;
}
function emptyHands(sampleId = 0, frameTimeMs = 0) {
return { sampleId, frameTimeMs, hands: HAND_SIDES.map((side) => ({ side, tracked: false, pose: null, grip: 0, trigger: 0, thumb: 0 })) };
}
function validateHandsSample(sample, metadata) {
if (!keys(sample, ["sampleId", "frameTimeMs", "hands", "arms"]) || !Number.isSafeInteger(sample.sampleId) || sample.sampleId < 0 || !Number.isFinite(sample.frameTimeMs) || sample.frameTimeMs < 0 || !Array.isArray(sample.hands) || sample.hands.length !== 2) throw new Error("Invalid hand sample");
for (const [i, hand] of sample.hands.entries()) {
if (!keys(hand, ["side", "tracked", "pose", "grip", "trigger", "thumb"]) || hand.side !== HAND_SIDES[i] || typeof hand.tracked !== "boolean" || !unit(hand.grip) || !unit(hand.trigger) || (hand.thumb !== void 0 || metadata?.modelVersion >= 4) && !unit(hand.thumb) || (hand.tracked ? !validHandPose(hand.pose) : hand.pose !== null)) throw new Error("Invalid hand pose");
}
if (sample.arms !== void 0) validateArmsSample(sample, metadata);
return sample;
}
function validateHandsMetadata(meta) {
if (!keys(meta, Object.keys(HANDS_METADATA)) || ![1, 2, 3, 4, 5].includes(meta.modelVersion) || ["version", "modelId", "space", "units", "poseConvention", "clock"].some((k) => meta[k] !== HANDS_METADATA[k]) || meta.grasp !== (meta.modelVersion >= 4 ? "controller-fingers" : "fixed-natural") || !["default-unmeasured", "user-adjusted", "device-calibrated"].includes(meta.calibration)) throw new Error("Invalid hand model version");
if (meta.appearance !== void 0 || meta.modelVersion >= 4) {
if (!keys(meta.appearance, ["color"]) || !/^#[\da-f]{6}$/i.test(meta.appearance.color)) throw new Error("Invalid hand model colors");
}
if (!keys(meta.dimensions, Object.keys(HANDS_METADATA.dimensions)) || Object.keys(HANDS_METADATA.dimensions).some((k) => !Number.isFinite(meta.dimensions[k]) || meta.dimensions[k] < 3e-3 || meta.dimensions[k] > 0.25) || !keys(meta.fixedPose, ["grip", "trigger"]) || !unit(meta.fixedPose.grip) || !unit(meta.fixedPose.trigger) || !keys(meta.gripToPalm, HAND_SIDES) || HAND_SIDES.some((side) => !validHandPose(meta.gripToPalm[side], 0.25))) throw new Error("Invalid hand model configuration");
if (meta.armModel !== void 0) {
const a = meta.armModel, defaults = HANDS_METADATA.armModel;
if (meta.modelVersion < 2 || !keys(a, Object.keys(defaults)) || ["version", "solver", "tracking"].some((k) => a[k] !== defaults[k]) || !vector(a.palmToWrist, 3) || a.palmToWrist.some((n) => Math.abs(n) > 0.25) || Object.keys(defaults).filter((k) => typeof defaults[k] === "number" && k !== "version").some((k) => !Number.isFinite(a[k]) || a[k] <= 0 || a[k] > (k === "poleSmoothing" ? 30 : k === "headYawDeadband" ? Math.PI : k === "maxTorsoTurnSpeed" ? 3 : 1)) || a.maxShoulderShift > 0.12) throw new Error("Invalid arm model configuration");
}
return meta;
}
function emptyArms() {
return { torsoYawRadians: null, joints: HAND_SIDES.map((side) => ({ side, status: "untracked", shoulder: null, elbow: null, wrist: null, shoulderShiftMeters: 0, reachErrorMeters: 0 })) };
}
function validateArmsSample(sample, metadata) {
const a = sample.arms, point = (p) => vector(p, 3) && p.every((n) => Math.abs(n) <= 100), distance = (p, q) => Math.hypot(...p.map((n, i) => n - q[i]));
if (metadata && !metadata.armModel) throw new Error("Arm model configuration is missing");
if (!keys(a, ["torsoYawRadians", "joints"]) || !(a.torsoYawRadians === null || Number.isFinite(a.torsoYawRadians) && Math.abs(a.torsoYawRadians) <= Math.PI) || !Array.isArray(a.joints) || a.joints.length !== 2) throw new Error("Invalid arm sample");
for (const [i, j] of a.joints.entries()) {
if (!keys(j, ["side", "status", "shoulder", "elbow", "wrist", "shoulderShiftMeters", "reachErrorMeters"]) || j.side !== HAND_SIDES[i] || !["untracked", "solved", "shoulder-adjusted", "forearm-only"].includes(j.status) || !Number.isFinite(j.shoulderShiftMeters) || j.shoulderShiftMeters < 0 || j.shoulderShiftMeters > 0.120001 || !Number.isFinite(j.reachErrorMeters) || j.reachErrorMeters < 0 || j.reachErrorMeters > 200) throw new Error("Invalid arm joint");
if (j.status === "untracked") {
if (j.shoulder !== null || j.elbow !== null || j.wrist !== null || j.shoulderShiftMeters !== 0 || j.reachErrorMeters !== 0) throw new Error("Untracked arms must be hidden");
continue;
}
if (!sample.hands[i].tracked || a.torsoYawRadians === null || ![j.shoulder, j.elbow, j.wrist].every(point)) throw new Error("Invalid arm tracking");
if (metadata) {
const m = metadata.armModel, p = sample.hands[i].pose, [x, y, z, w] = p.quaternionXYZW, [vx, vy, vz] = m.palmToWrist;
const tx = 2 * (y * vz - z * vy), ty = 2 * (z * vx - x * vz), tz = 2 * (x * vy - y * vx);
const wrist = [vx + w * tx + y * tz - z * ty, vy + w * ty + z * tx - x * tz, vz + w * tz + x * ty - y * tx].map((n, k) => n + p.positionMeters[k]);
if (distance(wrist, j.wrist) > 1e-4) throw new Error("Wrist and hand poses do not match");
if (j.shoulderShiftMeters > m.maxShoulderShift + 1e-5 || Math.abs(distance(j.elbow, j.wrist) - m.forearmLength) > 1e-4 || j.status !== "forearm-only" && (Math.abs(distance(j.shoulder, j.elbow) - m.upperArmLength) > 1e-4 || j.reachErrorMeters > 1e-4)) throw new Error("Invalid arm length");
}
}
}
function handMetadataForSize(scale = 1, armScale = 1, gripTilt = 45, presentationScale = 1) {
if (!Number.isFinite(scale) || scale < 0.7 || scale > 1.3) throw new Error("Hand size must be between 70% and 130%");
if (!Number.isFinite(armScale) || armScale < 0.8 || armScale > 1.2) throw new Error("Arm length must be between 80% and 120%");
if (!Number.isFinite(gripTilt) || gripTilt < 0 || gripTilt > 90) throw new Error("Grip tilt must be between 0° and 90°");
if (!Number.isFinite(presentationScale) || presentationScale < 0.5 || presentationScale > 1.5) throw new Error("Invalid body display scale");
scale *= presentationScale;
armScale *= presentationScale;
const m = structuredClone(HANDS_METADATA);
for (const k of Object.keys(m.dimensions)) m.dimensions[k] *= scale;
for (const side of HAND_SIDES) {
m.gripToPalm[side].positionMeters = m.gripToPalm[side].positionMeters.map((n) => n * scale);
m.gripToPalm[side].quaternionXYZW = fittedRotation(side, gripTilt);
}
m.armModel.palmToWrist = m.armModel.palmToWrist.map((n) => n * scale);
m.armModel.headToShoulderDrop = 0.21;
m.armModel.upperArmRadius *= presentationScale;
m.armModel.forearmRadius *= presentationScale;
m.armModel.upperArmLength *= armScale;
m.armModel.forearmLength *= armScale;
if (scale !== 1 || armScale !== 1 || gripTilt !== 45) m.calibration = "user-adjusted";
return m;
}
export {
HANDS_METADATA,
HAND_GRIP_BASIS,
HAND_SIDES,
emptyArms,
emptyHands,
handMetadataForSize,
validHandPose,
validateArmsSample,
validateHandsMetadata,
validateHandsSample
};
|