Files
mommysgoodpuppy fd19e37bbd Carry two-hand grabs in a frame spanning both controllers
A second hand dropped the window back to worker-driven motion and jumped it. Two-hand carries
now send the window relative to a frame built from both grab points (midpoint, the axis between
them, both hands' up, their distance as scale), which the renderer rebuilds from its fresh poses
of both controllers; release freezes that frame the same way. OpenVR screens attach to one
device only, so they stay absolute during a two-hand carry.

AGENTS.md now points agents at `deno task check`, which filters unrelated vendor errors.

Agent: Claude Code 2.1.296; model claude-opus-5-5; thread 941be8c7-aca1-4664-b284-7cc0eed69e21
2026-10-16 21:17:21 +03:00

78 lines
2.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Where a controller pose space sits on its OpenVR tracked device, for overlays that other
* processes position (desktop screens): while a controller carries a window they are set relative
* to the device, so the compositor moves them with its own fresh controller pose like the
* decoupled renderer does for the window itself.
*
* The webxr worker registers the resolver once OpenVR is up; without it everything stays absolute.
*/
import type { FollowPair, RenderAnchorSpace } from "../overlayRenderTracking.ts";
export type ControllerDeviceFrame = {
deviceIndex: number;
/** Column-major 4×4: the pose space (target ray or grip) in the device's own frame. */
deviceToPose: ArrayLike<number>;
};
let resolver: ((space: RenderAnchorSpace) => ControllerDeviceFrame | null) | null = null;
export function setControllerDeviceFrameResolver(next: typeof resolver): void {
resolver = next;
}
export function getControllerDeviceFrame(space: RenderAnchorSpace): ControllerDeviceFrame | null {
return resolver?.(space) ?? null;
}
/**
* Letting go of a carried window: the decoupled renderer freezes it at the controller pose it last
* showed and returns that pose, so the worker can land the window exactly there. `land` gets the
* pose (column-major 4×4), or null when no renderer answers in time.
*/
export type FollowReleaseSender = (
carry: { space: RenderAnchorSpace; pair?: FollowPair },
seq: number,
) => boolean;
let releaseSender: FollowReleaseSender | null = null;
let releaseSeq = 0;
const pendingReleases = new Map<number, (pose: ArrayLike<number> | null) => void>();
const FOLLOW_RELEASE_TIMEOUT_MS = 150;
export function setFollowReleaseSender(next: FollowReleaseSender | null): void {
releaseSender = next;
}
export function requestFollowRelease(
carry: { space: RenderAnchorSpace; pair?: FollowPair },
land: (pose: ArrayLike<number> | null) => void,
): void {
const seq = ++releaseSeq;
if (releaseSender == null || !releaseSender(carry, seq)) return land(null);
pendingReleases.set(seq, land);
setTimeout(() => settleFollowRelease(seq, null), FOLLOW_RELEASE_TIMEOUT_MS);
}
/** The renderer's answer (FOLLOWRELEASED); later or repeated answers are ignored. */
export function settleFollowRelease(seq: number, pose: ArrayLike<number> | null): void {
const land = pendingReleases.get(seq);
if (land == null) return;
pendingReleases.delete(seq);
land(pose);
}
/**
* Displays re-send their OpenVR pose at once after a landing, instead of on the next frame, so a
* screen attached to the controller lets go as soon as the window does.
*/
const displaySyncs = new Set<() => void>();
export function registerDisplaySync(sync: () => void): () => void {
displaySyncs.add(sync);
return () => displaySyncs.delete(sync);
}
export function syncDisplaysNow(): void {
for (const sync of displaySyncs) sync();
}