@realitycollective/webxr-interactions 0.1.1-preview.3 → 0.1.2-preview.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -2
- package/README.md +16 -2
- package/dist/behaviours/behaviour.d.ts +17 -1
- package/dist/behaviours/behaviour.js +12 -2
- package/dist/behaviours/behaviour.js.map +1 -1
- package/dist/behaviours/grab.d.ts +1 -0
- package/dist/behaviours/grab.js +10 -4
- package/dist/behaviours/grab.js.map +1 -1
- package/dist/contract-cases.js +15 -0
- package/dist/contract-cases.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts +8 -0
- package/dist/math.js +17 -0
- package/dist/math.js.map +1 -1
- package/dist/memory-ports.d.ts +55 -0
- package/dist/memory-ports.js +128 -0
- package/dist/memory-ports.js.map +1 -0
- package/dist/near-pointer-contract-cases.js +48 -0
- package/dist/near-pointer-contract-cases.js.map +1 -1
- package/dist/near-pointer.d.ts +9 -52
- package/dist/near-pointer.js +9 -23
- package/dist/near-pointer.js.map +1 -1
- package/dist/release-velocity.d.ts +58 -0
- package/dist/release-velocity.js +89 -0
- package/dist/release-velocity.js.map +1 -0
- package/dist/runtime.d.ts +38 -2
- package/dist/runtime.js +75 -15
- package/dist/runtime.js.map +1 -1
- package/dist/trace-synthetic.d.ts +12 -0
- package/dist/trace-synthetic.js +194 -0
- package/dist/trace-synthetic.js.map +1 -0
- package/dist/trace.d.ts +215 -0
- package/dist/trace.js +350 -0
- package/dist/trace.js.map +1 -0
- package/package.json +8 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-synthetic.js","sourceRoot":"","sources":["../src/trace-synthetic.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Synthetic traces: a scripted session run through the core itself over the\n * engine-free memory ports and recorded in the trace format. They exist to\n * prove the trace pipeline runs on every binding in CI (the recorder, the\n * replay, the comparison and each binding's subject) before a person has\n * recorded a real session on a Quest 3. Their `meta.synthetic` is true and\n * every case they produce says so: a synthetic trace proves nothing about\n * parity with the web, because its reference outcome is the core's own.\n */\nimport { NO_CAPABILITIES, type InputCapabilities, type InputProvider, type InputSourceSnapshot, type PoseTuple, type Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type { InteractableDescriptor } from \"./descriptor.js\";\nimport { quatFromAxisAngle } from \"./math.js\";\nimport { MemoryHitTester, MemoryTransformPort } from \"./memory-ports.js\";\nimport { InteractionRuntime } from \"./runtime.js\";\nimport { TraceRecorder, type InteractionTrace } from \"./trace.js\";\n\n/** The mechanics a synthetic trace can script, the playground's stations. */\nexport type SyntheticMechanic = \"press\" | \"grab-throw\" | \"dial\" | \"slide\" | \"hinge\";\n\nexport interface SynthesizeTraceOptions {\n mechanic: SyntheticMechanic;\n /** Frames per second of the scripted session. Default 72 (a Quest 3). */\n frameRate?: number;\n /** `\"hands\"` scripts a binary pinch, `\"controllers\"` an analog trigger and squeeze. Default hands. */\n input?: \"hands\" | \"controllers\";\n}\n\nclass ScriptedProvider implements InputProvider {\n sources: InputSourceSnapshot[] = [];\n private readonly capabilities: InputCapabilities = {\n ...NO_CAPABILITIES,\n rays: true,\n pokes: true,\n grabs: \"poseOnly\",\n handJoints: true,\n pinch: true,\n headPose: true,\n gaze: true,\n };\n\n getCapabilities(): InputCapabilities {\n return this.capabilities;\n }\n\n onCapabilitiesChanged(): () => void {\n return () => undefined;\n }\n\n onSourcesChanged(): () => void {\n return () => undefined;\n }\n\n sample(): readonly InputSourceSnapshot[] {\n return this.sources;\n }\n\n getHeadPose(): { position: Vec3Tuple; quaternion: [number, number, number, number] } {\n return { position: [0, 1.6, 0], quaternion: [0, 0, 0, 1] };\n }\n}\n\n/** The right hand at a point, pinching or not, its grip 6 cm behind the fingertip along +Z. */\nfunction hand(tip: Vec3Tuple, pinch: boolean, input: \"hands\" | \"controllers\", gripAt?: Vec3Tuple): InputSourceSnapshot {\n const grip = gripAt ?? [tip[0], tip[1], tip[2] + 0.06];\n return {\n id: \"right-hand\",\n kind: input === \"hands\" ? \"hand\" : \"controller\",\n handedness: \"right\",\n select: input === \"hands\" ? (pinch ? 1 : 0) : pinch ? 0.9 : 0,\n squeeze: input === \"hands\" ? 0 : pinch ? 0.9 : 0,\n ray: { origin: [grip[0], grip[1], grip[2]], direction: [0, 0, -1] },\n gripPose: { position: grip, quaternion: [0, 0, 0, 1] },\n indexTip: input === \"hands\" ? tip : grip,\n };\n}\n\nconst TARGET: Vec3Tuple = [0, 1.1, -0.5];\nconst RADIUS = 0.06;\n\n/** The station a mechanic uses, at the playground's toss position. */\nfunction target(mechanic: SyntheticMechanic): InteractableDescriptor {\n switch (mechanic) {\n case \"press\":\n return { id: \"pg-button\", behaviours: [{ kind: \"press\", axis: [0, 1, 0], travel: 0.045, depthFraction: 0.6 }] };\n case \"grab-throw\":\n return { id: \"pg-ball\", behaviours: [{ kind: \"grab\" }], pokeRadius: 0.09 };\n case \"dial\":\n return { id: \"pg-dial\", behaviours: [{ kind: \"dial\", axis: [0, 1, 0], maxAngle: (3 * Math.PI) / 2 }] };\n case \"slide\":\n return { id: \"pg-pulley\", behaviours: [{ kind: \"slide\", axis: [0, 1, 0], travel: 0.3 }] };\n case \"hinge\":\n return { id: \"pg-lever-table\", behaviours: [{ kind: \"hinge\", axis: [1, 0, 0], restDir: [0, 1, 0], maxAngle: (50 * Math.PI) / 180 }] };\n }\n}\n\n/** The scripted frames of a mechanic: the fingertip's path and whether the hand pinches. */\nfunction* script(mechanic: SyntheticMechanic, frameRate: number): Generator<{ tip: Vec3Tuple; pinch: boolean; grip?: Vec3Tuple }> {\n const n = (seconds: number) => Math.round(seconds * frameRate);\n const lerp = (a: Vec3Tuple, b: Vec3Tuple, k: number): Vec3Tuple => [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k];\n const far: Vec3Tuple = [0, 1.1, 0.2];\n const nearSurface: Vec3Tuple = [TARGET[0], TARGET[1], TARGET[2] + RADIUS + 0.01];\n switch (mechanic) {\n case \"press\": {\n const contact: Vec3Tuple = [TARGET[0], TARGET[1] + RADIUS - 0.01, TARGET[2]];\n const above: Vec3Tuple = [TARGET[0], TARGET[1] + RADIUS + 0.1, TARGET[2]];\n for (let i = 0; i < n(0.5); i += 1) yield { tip: lerp(far, above, i / n(0.5)), pinch: false };\n for (let i = 0; i <= n(0.3); i += 1) yield { tip: lerp(above, contact, i / n(0.3)), pinch: false };\n for (let i = 0; i < n(0.2); i += 1) yield { tip: contact, pinch: false };\n for (let i = 0; i <= n(0.3); i += 1) yield { tip: lerp(contact, above, i / n(0.3)), pinch: false };\n return;\n }\n case \"grab-throw\": {\n const gripAtBall: Vec3Tuple = [TARGET[0], TARGET[1], TARGET[2] + RADIUS + 0.02];\n const thrown: Vec3Tuple = [0, 1.4, -1.2];\n for (let i = 0; i < n(0.5); i += 1) yield { tip: lerp(far, gripAtBall, i / n(0.5)), pinch: false, grip: lerp(far, gripAtBall, i / n(0.5)) };\n for (let i = 0; i < n(0.2); i += 1) yield { tip: gripAtBall, pinch: true, grip: gripAtBall };\n // The throw: the grip accelerates forward over 0.25 s and lets go at speed.\n const throwFrames = n(0.25);\n for (let i = 0; i <= throwFrames; i += 1) {\n const k = i / throwFrames;\n const eased = k * k;\n yield { tip: lerp(gripAtBall, thrown, eased), pinch: i < throwFrames, grip: lerp(gripAtBall, thrown, eased) };\n }\n for (let i = 0; i < n(0.3); i += 1) yield { tip: thrown, pinch: false, grip: thrown };\n return;\n }\n case \"dial\": {\n // The grip circles the knob's axis a quarter turn, 5 cm out, while pinching.\n const r = RADIUS + 0.03;\n const at = (angle: number): Vec3Tuple => [TARGET[0] + r * Math.sin(angle), TARGET[1], TARGET[2] + r * Math.cos(angle)];\n for (let i = 0; i < n(0.4); i += 1) yield { tip: lerp(far, at(0), i / n(0.4)), pinch: false, grip: lerp(far, at(0), i / n(0.4)) };\n for (let i = 0; i < n(0.1); i += 1) yield { tip: at(0), pinch: true, grip: at(0) };\n for (let i = 0; i <= n(0.6); i += 1) yield { tip: at((Math.PI / 2) * (i / n(0.6))), pinch: true, grip: at((Math.PI / 2) * (i / n(0.6))) };\n for (let i = 0; i < n(0.2); i += 1) yield { tip: at(Math.PI / 2), pinch: false, grip: at(Math.PI / 2) };\n return;\n }\n case \"slide\": {\n const pulled: Vec3Tuple = [nearSurface[0], nearSurface[1] - 0.25, nearSurface[2]];\n for (let i = 0; i < n(0.4); i += 1) yield { tip: lerp(far, nearSurface, i / n(0.4)), pinch: false, grip: lerp(far, nearSurface, i / n(0.4)) };\n for (let i = 0; i < n(0.1); i += 1) yield { tip: nearSurface, pinch: true, grip: nearSurface };\n for (let i = 0; i <= n(0.5); i += 1) yield { tip: lerp(nearSurface, pulled, i / n(0.5)), pinch: true, grip: lerp(nearSurface, pulled, i / n(0.5)) };\n for (let i = 0; i < n(0.5); i += 1) yield { tip: pulled, pinch: false, grip: pulled };\n return;\n }\n case \"hinge\": {\n const swung: Vec3Tuple = [nearSurface[0], nearSurface[1] - 0.1, nearSurface[2] + 0.15];\n for (let i = 0; i < n(0.4); i += 1) yield { tip: lerp(far, nearSurface, i / n(0.4)), pinch: false, grip: lerp(far, nearSurface, i / n(0.4)) };\n for (let i = 0; i < n(0.1); i += 1) yield { tip: nearSurface, pinch: true, grip: nearSurface };\n for (let i = 0; i <= n(0.5); i += 1) yield { tip: lerp(nearSurface, swung, i / n(0.5)), pinch: true, grip: lerp(nearSurface, swung, i / n(0.5)) };\n for (let i = 0; i < n(0.2); i += 1) yield { tip: swung, pinch: false, grip: swung };\n return;\n }\n }\n}\n\n/** Run a scripted mechanic through the core over memory ports and record it. */\nexport function synthesizeTrace(options: SynthesizeTraceOptions): InteractionTrace {\n const frameRate = options.frameRate ?? 72;\n const input = options.input ?? \"hands\";\n const dt = 1 / frameRate;\n const descriptor = target(options.mechanic);\n const rest: PoseTuple = { position: [TARGET[0], TARGET[1], TARGET[2]], quaternion: options.mechanic === \"hinge\" ? quatFromAxisAngle([0, 1, 0], 0) : [0, 0, 0, 1] };\n const hitTester = new MemoryHitTester();\n const port = new MemoryTransformPort(rest);\n hitTester.place(descriptor.id, rest.position, RADIUS, port);\n const provider = new ScriptedProvider();\n const runtime = new InteractionRuntime({ provider, hitTester });\n runtime.registerInteractable(descriptor, { transform: port });\n const recorder = new TraceRecorder(runtime, {\n meta: {\n platform: \"core\",\n device: \"synthetic\",\n mechanic: options.mechanic,\n input,\n synthetic: true,\n notes: \"A scripted session through the core over memory ports. Proves the trace pipeline, never parity with the web.\",\n },\n targets: [{ descriptor, port, radius: RADIUS }],\n head: () => provider.getHeadPose(),\n releases: () => {\n const last = port.releases[port.releases.length - 1];\n if (!last || port.releases.length === releasesSeen) return undefined;\n releasesSeen = port.releases.length;\n return { [descriptor.id]: last };\n },\n });\n let releasesSeen = 0;\n for (const frame of script(options.mechanic, frameRate)) {\n provider.sources = [hand(frame.tip, frame.pinch, input, frame.grip)];\n runtime.update(dt);\n recorder.endFrame(dt);\n }\n const trace = recorder.stop();\n runtime.dispose();\n return trace;\n}\n"]}
|
package/dist/trace.d.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recorded input traces - parity as a number, checkable without a device
|
|
3
|
+
* per platform (Pale Signal handover of 29 September 2026, G6).
|
|
4
|
+
*
|
|
5
|
+
* The host conformance kits feed ideal scripted values: exact thresholds,
|
|
6
|
+
* perfect poses, an empty world. They pass on native while a person still
|
|
7
|
+
* finds the mechanics broken, because real hands are not ideal. A TRACE is
|
|
8
|
+
* a real session, recorded frame by frame on the reference platform (IWSDK
|
|
9
|
+
* on a Quest 3 in the Quest Browser): each frame's input source snapshots,
|
|
10
|
+
* the head pose, and the reference's own outcome, the events it raised
|
|
11
|
+
* (press and grab start and end), the values its behaviours published (the
|
|
12
|
+
* dial's angle, the slide's pull), the poses its objects took, and the
|
|
13
|
+
* velocity a thrown object left the hand with.
|
|
14
|
+
*
|
|
15
|
+
* Replaying the trace's INPUT through a binding's core (the same runtime,
|
|
16
|
+
* over that binding's own hit tester and transform port) and comparing the
|
|
17
|
+
* outcome with the reference's, frame for frame, within stated tolerances,
|
|
18
|
+
* is what proves the binding's rules match the web on the input a person
|
|
19
|
+
* actually produced. `traceParityCases()` is that comparison as a
|
|
20
|
+
* runner-free suite every binding runs in CI over the synthetic traces
|
|
21
|
+
* (`synthesizeTrace()`), and locally over recorded ones.
|
|
22
|
+
*
|
|
23
|
+
* The format is plain JSON (`rc-trace.v1`), so a trace recorded in a browser
|
|
24
|
+
* can be downloaded and replayed as it is.
|
|
25
|
+
*/
|
|
26
|
+
import { type HeadPose, type InputCapabilities, type InputHitHint, type InputProvider, type InputSourceSnapshot, type PoseTuple, type Unsubscribe } from "@realitycollective/webxr-input";
|
|
27
|
+
import type { InteractableDescriptor } from "./descriptor.js";
|
|
28
|
+
import type { InteractionEvent } from "./events.js";
|
|
29
|
+
import type { HoldRelease, TransformPort } from "./ports.js";
|
|
30
|
+
import { InteractionRuntime, type InteractionRuntimeOptions } from "./runtime.js";
|
|
31
|
+
/** The trace format version this module writes and reads. */
|
|
32
|
+
export declare const TRACE_VERSION = "rc-trace.v1";
|
|
33
|
+
/** An interactable as the reference registered it, so a replay can rebuild the scene. */
|
|
34
|
+
export interface TraceTarget {
|
|
35
|
+
descriptor: InteractableDescriptor;
|
|
36
|
+
/** The target's rest pose, world space, at the start of the trace. */
|
|
37
|
+
restPose: PoseTuple;
|
|
38
|
+
/** The radius the reference hit-tests it with, metres (IWSDK: `targetRadius`, default 0.1). */
|
|
39
|
+
radius: number;
|
|
40
|
+
}
|
|
41
|
+
/** The reference's outcome on one frame. */
|
|
42
|
+
export interface TraceReference {
|
|
43
|
+
/** The events the reference raised this frame (type, interactable, interactor). */
|
|
44
|
+
events: Array<Pick<InteractionEvent, "type" | "interactableId" | "interactorId" | "behaviourKind">>;
|
|
45
|
+
/** Each registered target's primary value after the frame (a dial's 0..1, a slide's 0..1). */
|
|
46
|
+
values: Record<string, number>;
|
|
47
|
+
/** Each target's world pose after the frame. */
|
|
48
|
+
poses: Record<string, PoseTuple>;
|
|
49
|
+
/** A release velocity the reference handed a target's physics this frame. */
|
|
50
|
+
releases?: Record<string, HoldRelease>;
|
|
51
|
+
}
|
|
52
|
+
export interface TraceFrame {
|
|
53
|
+
/** Seconds since the trace began. */
|
|
54
|
+
t: number;
|
|
55
|
+
/** Seconds since the previous frame. */
|
|
56
|
+
dt: number;
|
|
57
|
+
head?: HeadPose;
|
|
58
|
+
sources: InputSourceSnapshot[];
|
|
59
|
+
hints?: InputHitHint[];
|
|
60
|
+
reference: TraceReference;
|
|
61
|
+
}
|
|
62
|
+
export interface TraceMeta {
|
|
63
|
+
/** The platform the trace was recorded on: `"iwsdk"` is the reference. */
|
|
64
|
+
platform: string;
|
|
65
|
+
/** ISO date and time of the recording. */
|
|
66
|
+
recordedAt: string;
|
|
67
|
+
/** The device, as the recorder knew it (`"Quest 3"`, `"synthetic"`). */
|
|
68
|
+
device: string;
|
|
69
|
+
/** The mechanic the person performed: `"press"`, `"grab"`, `"dial"`, `"slide"`, `"hinge"`, `"throw"`. */
|
|
70
|
+
mechanic: string;
|
|
71
|
+
/** What the person used. */
|
|
72
|
+
input: "hands" | "controllers";
|
|
73
|
+
/** Package versions in the recording app, for the record. */
|
|
74
|
+
packages?: Record<string, string>;
|
|
75
|
+
/** True for a trace a program made up rather than a person recorded; such a trace proves the pipeline, never parity. */
|
|
76
|
+
synthetic?: boolean;
|
|
77
|
+
notes?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface InteractionTrace {
|
|
80
|
+
version: typeof TRACE_VERSION;
|
|
81
|
+
meta: TraceMeta;
|
|
82
|
+
/** The capabilities the reference reported, so the replay negotiates the same behaviours. */
|
|
83
|
+
capabilities: InputCapabilities;
|
|
84
|
+
targets: TraceTarget[];
|
|
85
|
+
frames: TraceFrame[];
|
|
86
|
+
}
|
|
87
|
+
export interface TraceRecorderOptions {
|
|
88
|
+
meta: Omit<TraceMeta, "recordedAt"> & {
|
|
89
|
+
recordedAt?: string;
|
|
90
|
+
};
|
|
91
|
+
/** The targets to record, with a port to read each one's pose and a way to read its primary value. */
|
|
92
|
+
targets: Array<{
|
|
93
|
+
descriptor: InteractableDescriptor;
|
|
94
|
+
port: TransformPort;
|
|
95
|
+
radius?: number;
|
|
96
|
+
}>;
|
|
97
|
+
/** The head pose each frame, when the recorder has one. */
|
|
98
|
+
head?: () => HeadPose | undefined;
|
|
99
|
+
/** A release the reference's physics received this frame, per target, when the recorder can read one. */
|
|
100
|
+
releases?: () => Record<string, HoldRelease> | undefined;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Records a trace from a live runtime: attach it, play the mechanic, then
|
|
104
|
+
* `stop()` and keep the JSON. It listens to the runtime's sampled sources
|
|
105
|
+
* (the input) and its events (the outcome), and reads each target's value
|
|
106
|
+
* and pose after every frame. On IWSDK the runtime's grab events mirror
|
|
107
|
+
* IWSDK's own `Grabbed` tag (through hints and `nativeGrabbing`), so the
|
|
108
|
+
* events recorded there ARE the engine's outcome.
|
|
109
|
+
*/
|
|
110
|
+
export declare class TraceRecorder {
|
|
111
|
+
private readonly frames;
|
|
112
|
+
private pending;
|
|
113
|
+
private pendingEvents;
|
|
114
|
+
private elapsed;
|
|
115
|
+
private readonly unsubscribe;
|
|
116
|
+
private readonly options;
|
|
117
|
+
private readonly runtime;
|
|
118
|
+
private stopped;
|
|
119
|
+
constructor(runtime: InteractionRuntime, options: TraceRecorderOptions);
|
|
120
|
+
/** Call once per frame AFTER `runtime.update(dt)`, with the same dt. */
|
|
121
|
+
endFrame(dt: number): void;
|
|
122
|
+
private onSample;
|
|
123
|
+
/** How many frames are recorded so far. */
|
|
124
|
+
get length(): number;
|
|
125
|
+
/** Stop listening and return the trace. Further frames are ignored. */
|
|
126
|
+
stop(): InteractionTrace;
|
|
127
|
+
}
|
|
128
|
+
/** A provider that plays a trace's frames back, one per `sample()`. */
|
|
129
|
+
export declare class TraceInputProvider implements InputProvider {
|
|
130
|
+
private index;
|
|
131
|
+
private readonly trace;
|
|
132
|
+
private readonly capsListeners;
|
|
133
|
+
constructor(trace: InteractionTrace);
|
|
134
|
+
/** Advance to the next frame; returns it, or null past the end. */
|
|
135
|
+
next(): TraceFrame | null;
|
|
136
|
+
/** The frame `sample()` currently reports. */
|
|
137
|
+
get frame(): TraceFrame | null;
|
|
138
|
+
getCapabilities(): InputCapabilities;
|
|
139
|
+
onCapabilitiesChanged(listener: (c: InputCapabilities) => void): Unsubscribe;
|
|
140
|
+
onSourcesChanged(): Unsubscribe;
|
|
141
|
+
sample(): readonly InputSourceSnapshot[];
|
|
142
|
+
sampleHints(): readonly InputHitHint[];
|
|
143
|
+
getHeadPose(): HeadPose;
|
|
144
|
+
}
|
|
145
|
+
/** What a binding provides so a trace can be replayed through its own ports. */
|
|
146
|
+
export interface TraceReplaySubject {
|
|
147
|
+
/**
|
|
148
|
+
* Build the trace's targets in the binding's scene and register them with
|
|
149
|
+
* the runtime the replay hands over. The subject owns the hit tester the
|
|
150
|
+
* runtime was built with (`hitTester` on the options it returned).
|
|
151
|
+
*/
|
|
152
|
+
build(trace: InteractionTrace, runtime: InteractionRuntime): {
|
|
153
|
+
ports: Record<string, TransformPort>;
|
|
154
|
+
dispose?: () => void;
|
|
155
|
+
};
|
|
156
|
+
/** Runtime options beyond the provider (the binding's hit tester, near-pointer options). */
|
|
157
|
+
runtimeOptions(trace: InteractionTrace): Omit<InteractionRuntimeOptions, "provider">;
|
|
158
|
+
}
|
|
159
|
+
/** A frame index on which a given event happened, for a target. */
|
|
160
|
+
export interface TraceEventTiming {
|
|
161
|
+
type: string;
|
|
162
|
+
interactableId: string;
|
|
163
|
+
frame: number;
|
|
164
|
+
t: number;
|
|
165
|
+
}
|
|
166
|
+
/** The outcome of one replay, in the same terms as the reference. */
|
|
167
|
+
export interface TraceOutcome {
|
|
168
|
+
events: TraceEventTiming[];
|
|
169
|
+
/** Per target, the value after each frame. */
|
|
170
|
+
values: Record<string, number[]>;
|
|
171
|
+
/** Per target, the world pose after each frame. */
|
|
172
|
+
poses: Record<string, PoseTuple[]>;
|
|
173
|
+
/** Per target, the last release velocity `endHold` received. */
|
|
174
|
+
releases: Record<string, HoldRelease>;
|
|
175
|
+
}
|
|
176
|
+
/** Replay a trace's input through a runtime built over the subject's ports and collect the outcome. */
|
|
177
|
+
export declare function replayTrace(trace: InteractionTrace, subject: TraceReplaySubject): TraceOutcome;
|
|
178
|
+
/** The reference outcome of a trace in the same terms as a replay's. */
|
|
179
|
+
export declare function referenceOutcome(trace: InteractionTrace): TraceOutcome;
|
|
180
|
+
export interface TraceTolerances {
|
|
181
|
+
/** Frames a press or grab start or end may differ by. Default 1 (one frame at 72 Hz, 14 ms). */
|
|
182
|
+
eventFrames: number;
|
|
183
|
+
/** Absolute difference allowed in a target's 0..1 value on any frame. Default 0.02. */
|
|
184
|
+
value: number;
|
|
185
|
+
/** Metres a target's position may differ by on any frame. Default 0.01. */
|
|
186
|
+
position: number;
|
|
187
|
+
/** Fraction of the reference release speed the replay may differ by, or `releaseFloor` m/s, whichever is larger. Default 0.1. */
|
|
188
|
+
releaseFraction: number;
|
|
189
|
+
/** Metres per second below which two release speeds count as equal. Default 0.1. */
|
|
190
|
+
releaseFloor: number;
|
|
191
|
+
}
|
|
192
|
+
export declare const TRACE_TOLERANCES: Readonly<TraceTolerances>;
|
|
193
|
+
/** One difference between a replay and the reference. */
|
|
194
|
+
export interface TraceDifference {
|
|
195
|
+
kind: "event-missing" | "event-extra" | "event-late" | "value" | "position" | "release";
|
|
196
|
+
interactableId: string;
|
|
197
|
+
detail: string;
|
|
198
|
+
}
|
|
199
|
+
/** Compare a replay's outcome with the reference's within the tolerances. */
|
|
200
|
+
export declare function compareOutcomes(reference: TraceOutcome, replay: TraceOutcome, tolerances?: Partial<TraceTolerances>): TraceDifference[];
|
|
201
|
+
/** One check a binding must pass over one trace. */
|
|
202
|
+
export interface TraceParityCase {
|
|
203
|
+
name: string;
|
|
204
|
+
trace: InteractionTrace;
|
|
205
|
+
run(subject: TraceReplaySubject): void;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The trace parity suite over a set of traces: for each, replay through the
|
|
209
|
+
* subject and compare with the reference. A case fails with every difference
|
|
210
|
+
* listed. A synthetic trace's case names itself as such: it proves the
|
|
211
|
+
* pipeline runs on that platform, not parity with the web.
|
|
212
|
+
*/
|
|
213
|
+
export declare function traceParityCases(traces: readonly InteractionTrace[], tolerances?: Partial<TraceTolerances>): TraceParityCase[];
|
|
214
|
+
/** Check a parsed JSON value is a trace this module can replay. */
|
|
215
|
+
export declare function isInteractionTrace(value: unknown): value is InteractionTrace;
|
package/dist/trace.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recorded input traces - parity as a number, checkable without a device
|
|
3
|
+
* per platform (Pale Signal handover of 29 September 2026, G6).
|
|
4
|
+
*
|
|
5
|
+
* The host conformance kits feed ideal scripted values: exact thresholds,
|
|
6
|
+
* perfect poses, an empty world. They pass on native while a person still
|
|
7
|
+
* finds the mechanics broken, because real hands are not ideal. A TRACE is
|
|
8
|
+
* a real session, recorded frame by frame on the reference platform (IWSDK
|
|
9
|
+
* on a Quest 3 in the Quest Browser): each frame's input source snapshots,
|
|
10
|
+
* the head pose, and the reference's own outcome, the events it raised
|
|
11
|
+
* (press and grab start and end), the values its behaviours published (the
|
|
12
|
+
* dial's angle, the slide's pull), the poses its objects took, and the
|
|
13
|
+
* velocity a thrown object left the hand with.
|
|
14
|
+
*
|
|
15
|
+
* Replaying the trace's INPUT through a binding's core (the same runtime,
|
|
16
|
+
* over that binding's own hit tester and transform port) and comparing the
|
|
17
|
+
* outcome with the reference's, frame for frame, within stated tolerances,
|
|
18
|
+
* is what proves the binding's rules match the web on the input a person
|
|
19
|
+
* actually produced. `traceParityCases()` is that comparison as a
|
|
20
|
+
* runner-free suite every binding runs in CI over the synthetic traces
|
|
21
|
+
* (`synthesizeTrace()`), and locally over recorded ones.
|
|
22
|
+
*
|
|
23
|
+
* The format is plain JSON (`rc-trace.v1`), so a trace recorded in a browser
|
|
24
|
+
* can be downloaded and replayed as it is.
|
|
25
|
+
*/
|
|
26
|
+
import { NO_CAPABILITIES, } from "@realitycollective/webxr-input";
|
|
27
|
+
import { InteractionRuntime } from "./runtime.js";
|
|
28
|
+
/** The trace format version this module writes and reads. */
|
|
29
|
+
export const TRACE_VERSION = "rc-trace.v1";
|
|
30
|
+
/**
|
|
31
|
+
* Records a trace from a live runtime: attach it, play the mechanic, then
|
|
32
|
+
* `stop()` and keep the JSON. It listens to the runtime's sampled sources
|
|
33
|
+
* (the input) and its events (the outcome), and reads each target's value
|
|
34
|
+
* and pose after every frame. On IWSDK the runtime's grab events mirror
|
|
35
|
+
* IWSDK's own `Grabbed` tag (through hints and `nativeGrabbing`), so the
|
|
36
|
+
* events recorded there ARE the engine's outcome.
|
|
37
|
+
*/
|
|
38
|
+
export class TraceRecorder {
|
|
39
|
+
frames = [];
|
|
40
|
+
pending = null;
|
|
41
|
+
pendingEvents = [];
|
|
42
|
+
elapsed = 0;
|
|
43
|
+
unsubscribe = [];
|
|
44
|
+
options;
|
|
45
|
+
runtime;
|
|
46
|
+
stopped = false;
|
|
47
|
+
constructor(runtime, options) {
|
|
48
|
+
this.runtime = runtime;
|
|
49
|
+
this.options = options;
|
|
50
|
+
this.unsubscribe.push(runtime.onSample((sources) => this.onSample(sources)), runtime.onEvent((event) => {
|
|
51
|
+
this.pendingEvents.push({
|
|
52
|
+
type: event.type,
|
|
53
|
+
interactableId: event.interactableId,
|
|
54
|
+
...(event.interactorId !== undefined ? { interactorId: event.interactorId } : {}),
|
|
55
|
+
...(event.behaviourKind !== undefined ? { behaviourKind: event.behaviourKind } : {}),
|
|
56
|
+
});
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
/** Call once per frame AFTER `runtime.update(dt)`, with the same dt. */
|
|
60
|
+
endFrame(dt) {
|
|
61
|
+
if (this.stopped || !this.pending)
|
|
62
|
+
return;
|
|
63
|
+
const values = {};
|
|
64
|
+
const poses = {};
|
|
65
|
+
for (const target of this.options.targets) {
|
|
66
|
+
const id = target.descriptor.id;
|
|
67
|
+
values[id] = this.runtime.getState(id)?.value ?? 0;
|
|
68
|
+
const pose = target.port.getWorldPose();
|
|
69
|
+
poses[id] = { position: [...pose.position], quaternion: [...pose.quaternion] };
|
|
70
|
+
}
|
|
71
|
+
const releases = this.options.releases?.();
|
|
72
|
+
const frame = {
|
|
73
|
+
...this.pending,
|
|
74
|
+
dt,
|
|
75
|
+
reference: { events: this.pendingEvents, values, poses, ...(releases && Object.keys(releases).length > 0 ? { releases } : {}) },
|
|
76
|
+
};
|
|
77
|
+
this.frames.push(frame);
|
|
78
|
+
this.pending = null;
|
|
79
|
+
this.pendingEvents = [];
|
|
80
|
+
}
|
|
81
|
+
onSample(sources) {
|
|
82
|
+
if (this.stopped)
|
|
83
|
+
return;
|
|
84
|
+
const head = this.options.head?.();
|
|
85
|
+
const hints = this.runtime.getProvider().sampleHints?.();
|
|
86
|
+
this.pending = {
|
|
87
|
+
t: this.elapsed,
|
|
88
|
+
dt: 0,
|
|
89
|
+
...(head ? { head: { position: [...head.position], quaternion: [...head.quaternion] } } : {}),
|
|
90
|
+
sources: sources.map(cloneSnapshot),
|
|
91
|
+
...(hints && hints.length > 0 ? { hints: hints.map((hint) => ({ ...hint })) } : {}),
|
|
92
|
+
};
|
|
93
|
+
this.pendingEvents = [];
|
|
94
|
+
// The frame's dt is known at endFrame; the elapsed time advances there.
|
|
95
|
+
}
|
|
96
|
+
/** How many frames are recorded so far. */
|
|
97
|
+
get length() {
|
|
98
|
+
return this.frames.length;
|
|
99
|
+
}
|
|
100
|
+
/** Stop listening and return the trace. Further frames are ignored. */
|
|
101
|
+
stop() {
|
|
102
|
+
this.stopped = true;
|
|
103
|
+
for (const off of this.unsubscribe)
|
|
104
|
+
off();
|
|
105
|
+
let elapsed = 0;
|
|
106
|
+
for (const frame of this.frames) {
|
|
107
|
+
frame.t = elapsed;
|
|
108
|
+
elapsed += frame.dt;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
version: TRACE_VERSION,
|
|
112
|
+
meta: { recordedAt: this.options.meta.recordedAt ?? new Date().toISOString(), ...this.options.meta },
|
|
113
|
+
capabilities: { ...this.runtime.getCapabilities() },
|
|
114
|
+
targets: this.options.targets.map((target) => ({
|
|
115
|
+
descriptor: target.descriptor,
|
|
116
|
+
restPose: target.port.getRestWorldPose(),
|
|
117
|
+
radius: target.radius ?? 0.1,
|
|
118
|
+
})),
|
|
119
|
+
frames: this.frames,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function cloneSnapshot(source) {
|
|
124
|
+
return JSON.parse(JSON.stringify(source));
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Replay
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
/** A provider that plays a trace's frames back, one per `sample()`. */
|
|
130
|
+
export class TraceInputProvider {
|
|
131
|
+
index = -1;
|
|
132
|
+
trace;
|
|
133
|
+
capsListeners = new Set();
|
|
134
|
+
constructor(trace) {
|
|
135
|
+
this.trace = trace;
|
|
136
|
+
}
|
|
137
|
+
/** Advance to the next frame; returns it, or null past the end. */
|
|
138
|
+
next() {
|
|
139
|
+
this.index += 1;
|
|
140
|
+
return this.trace.frames[this.index] ?? null;
|
|
141
|
+
}
|
|
142
|
+
/** The frame `sample()` currently reports. */
|
|
143
|
+
get frame() {
|
|
144
|
+
return this.trace.frames[this.index] ?? null;
|
|
145
|
+
}
|
|
146
|
+
getCapabilities() {
|
|
147
|
+
return { ...NO_CAPABILITIES, ...this.trace.capabilities };
|
|
148
|
+
}
|
|
149
|
+
onCapabilitiesChanged(listener) {
|
|
150
|
+
this.capsListeners.add(listener);
|
|
151
|
+
return () => {
|
|
152
|
+
this.capsListeners.delete(listener);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
onSourcesChanged() {
|
|
156
|
+
return () => undefined;
|
|
157
|
+
}
|
|
158
|
+
sample() {
|
|
159
|
+
return this.frame?.sources.map(cloneSnapshot) ?? [];
|
|
160
|
+
}
|
|
161
|
+
sampleHints() {
|
|
162
|
+
return this.frame?.hints ?? [];
|
|
163
|
+
}
|
|
164
|
+
getHeadPose() {
|
|
165
|
+
return this.frame?.head ?? { position: [0, 1.6, 0], quaternion: [0, 0, 0, 1] };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Replay a trace's input through a runtime built over the subject's ports and collect the outcome. */
|
|
169
|
+
export function replayTrace(trace, subject) {
|
|
170
|
+
const provider = new TraceInputProvider(trace);
|
|
171
|
+
const runtime = new InteractionRuntime({ provider, ...subject.runtimeOptions(trace) });
|
|
172
|
+
const built = subject.build(trace, runtime);
|
|
173
|
+
const outcome = { events: [], values: {}, poses: {}, releases: {} };
|
|
174
|
+
for (const target of trace.targets) {
|
|
175
|
+
outcome.values[target.descriptor.id] = [];
|
|
176
|
+
outcome.poses[target.descriptor.id] = [];
|
|
177
|
+
const port = built.ports[target.descriptor.id];
|
|
178
|
+
if (port && port.endHold) {
|
|
179
|
+
const original = port.endHold.bind(port);
|
|
180
|
+
port.endHold = (release) => {
|
|
181
|
+
outcome.releases[target.descriptor.id] = { linearVelocity: [...release.linearVelocity], angularVelocity: [...release.angularVelocity] };
|
|
182
|
+
original(release);
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
let frameIndex = -1;
|
|
187
|
+
const off = runtime.onEvent((event) => {
|
|
188
|
+
outcome.events.push({ type: event.type, interactableId: event.interactableId, frame: frameIndex, t: trace.frames[frameIndex]?.t ?? 0 });
|
|
189
|
+
});
|
|
190
|
+
try {
|
|
191
|
+
let frame = provider.next();
|
|
192
|
+
while (frame) {
|
|
193
|
+
frameIndex += 1;
|
|
194
|
+
runtime.update(frame.dt);
|
|
195
|
+
for (const target of trace.targets) {
|
|
196
|
+
const id = target.descriptor.id;
|
|
197
|
+
outcome.values[id].push(runtime.getState(id)?.value ?? 0);
|
|
198
|
+
const pose = built.ports[id]?.getWorldPose() ?? { position: [0, 0, 0], quaternion: [0, 0, 0, 1] };
|
|
199
|
+
outcome.poses[id].push({ position: [...pose.position], quaternion: [...pose.quaternion] });
|
|
200
|
+
}
|
|
201
|
+
frame = provider.next();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
off();
|
|
206
|
+
runtime.dispose();
|
|
207
|
+
built.dispose?.();
|
|
208
|
+
}
|
|
209
|
+
return outcome;
|
|
210
|
+
}
|
|
211
|
+
/** The reference outcome of a trace in the same terms as a replay's. */
|
|
212
|
+
export function referenceOutcome(trace) {
|
|
213
|
+
const outcome = { events: [], values: {}, poses: {}, releases: {} };
|
|
214
|
+
for (const target of trace.targets) {
|
|
215
|
+
outcome.values[target.descriptor.id] = [];
|
|
216
|
+
outcome.poses[target.descriptor.id] = [];
|
|
217
|
+
}
|
|
218
|
+
trace.frames.forEach((frame, index) => {
|
|
219
|
+
for (const event of frame.reference.events) {
|
|
220
|
+
outcome.events.push({ type: event.type, interactableId: event.interactableId, frame: index, t: frame.t });
|
|
221
|
+
}
|
|
222
|
+
for (const target of trace.targets) {
|
|
223
|
+
const id = target.descriptor.id;
|
|
224
|
+
outcome.values[id].push(frame.reference.values[id] ?? 0);
|
|
225
|
+
outcome.poses[id].push(frame.reference.poses[id] ?? { position: [0, 0, 0], quaternion: [0, 0, 0, 1] });
|
|
226
|
+
}
|
|
227
|
+
for (const [id, release] of Object.entries(frame.reference.releases ?? {}))
|
|
228
|
+
outcome.releases[id] = release;
|
|
229
|
+
});
|
|
230
|
+
return outcome;
|
|
231
|
+
}
|
|
232
|
+
export const TRACE_TOLERANCES = Object.freeze({
|
|
233
|
+
eventFrames: 1,
|
|
234
|
+
value: 0.02,
|
|
235
|
+
position: 0.01,
|
|
236
|
+
releaseFraction: 0.1,
|
|
237
|
+
releaseFloor: 0.1,
|
|
238
|
+
});
|
|
239
|
+
/** The events whose timing parity compares. */
|
|
240
|
+
const TIMED_EVENTS = new Set(["pressStart", "pressEnd", "grabStart", "grabEnd"]);
|
|
241
|
+
/** Compare a replay's outcome with the reference's within the tolerances. */
|
|
242
|
+
export function compareOutcomes(reference, replay, tolerances = {}) {
|
|
243
|
+
const tol = { ...TRACE_TOLERANCES, ...tolerances };
|
|
244
|
+
const differences = [];
|
|
245
|
+
const refEvents = reference.events.filter((e) => TIMED_EVENTS.has(e.type));
|
|
246
|
+
const repEvents = replay.events.filter((e) => TIMED_EVENTS.has(e.type));
|
|
247
|
+
const used = new Set();
|
|
248
|
+
for (const ref of refEvents) {
|
|
249
|
+
let best = -1;
|
|
250
|
+
let bestDelta = Number.POSITIVE_INFINITY;
|
|
251
|
+
repEvents.forEach((rep, index) => {
|
|
252
|
+
if (used.has(index) || rep.type !== ref.type || rep.interactableId !== ref.interactableId)
|
|
253
|
+
return;
|
|
254
|
+
const delta = Math.abs(rep.frame - ref.frame);
|
|
255
|
+
if (delta < bestDelta) {
|
|
256
|
+
bestDelta = delta;
|
|
257
|
+
best = index;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
if (best < 0) {
|
|
261
|
+
differences.push({ kind: "event-missing", interactableId: ref.interactableId, detail: `${ref.type} on frame ${ref.frame} (t=${ref.t.toFixed(3)} s) never happened in the replay` });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
used.add(best);
|
|
265
|
+
if (bestDelta > tol.eventFrames) {
|
|
266
|
+
differences.push({ kind: "event-late", interactableId: ref.interactableId, detail: `${ref.type}: reference frame ${ref.frame}, replay frame ${repEvents[best].frame} (${bestDelta} frames apart, ${tol.eventFrames} allowed)` });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
repEvents.forEach((rep, index) => {
|
|
270
|
+
if (!used.has(index))
|
|
271
|
+
differences.push({ kind: "event-extra", interactableId: rep.interactableId, detail: `${rep.type} on frame ${rep.frame} happened in the replay but not in the reference` });
|
|
272
|
+
});
|
|
273
|
+
for (const [id, values] of Object.entries(reference.values)) {
|
|
274
|
+
const replayed = replay.values[id] ?? [];
|
|
275
|
+
let worst = 0;
|
|
276
|
+
let worstFrame = -1;
|
|
277
|
+
values.forEach((value, frame) => {
|
|
278
|
+
const delta = Math.abs((replayed[frame] ?? 0) - value);
|
|
279
|
+
if (delta > worst) {
|
|
280
|
+
worst = delta;
|
|
281
|
+
worstFrame = frame;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
if (worst > tol.value)
|
|
285
|
+
differences.push({ kind: "value", interactableId: id, detail: `value differs by ${worst.toFixed(4)} on frame ${worstFrame} (${tol.value} allowed)` });
|
|
286
|
+
}
|
|
287
|
+
for (const [id, poses] of Object.entries(reference.poses)) {
|
|
288
|
+
const replayed = replay.poses[id] ?? [];
|
|
289
|
+
let worst = 0;
|
|
290
|
+
let worstFrame = -1;
|
|
291
|
+
poses.forEach((pose, frame) => {
|
|
292
|
+
const other = replayed[frame];
|
|
293
|
+
if (!other)
|
|
294
|
+
return;
|
|
295
|
+
const delta = Math.hypot(other.position[0] - pose.position[0], other.position[1] - pose.position[1], other.position[2] - pose.position[2]);
|
|
296
|
+
if (delta > worst) {
|
|
297
|
+
worst = delta;
|
|
298
|
+
worstFrame = frame;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
if (worst > tol.position)
|
|
302
|
+
differences.push({ kind: "position", interactableId: id, detail: `position differs by ${worst.toFixed(4)} m on frame ${worstFrame} (${tol.position} allowed)` });
|
|
303
|
+
}
|
|
304
|
+
for (const [id, release] of Object.entries(reference.releases)) {
|
|
305
|
+
const other = replay.releases[id];
|
|
306
|
+
const refSpeed = Math.hypot(...release.linearVelocity);
|
|
307
|
+
if (!other) {
|
|
308
|
+
differences.push({ kind: "release", interactableId: id, detail: `the reference released at ${refSpeed.toFixed(2)} m/s; the replay handed its physics no release` });
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const speed = Math.hypot(...other.linearVelocity);
|
|
312
|
+
const allowed = Math.max(tol.releaseFloor, refSpeed * tol.releaseFraction);
|
|
313
|
+
const delta = Math.hypot(other.linearVelocity[0] - release.linearVelocity[0], other.linearVelocity[1] - release.linearVelocity[1], other.linearVelocity[2] - release.linearVelocity[2]);
|
|
314
|
+
if (delta > allowed) {
|
|
315
|
+
differences.push({ kind: "release", interactableId: id, detail: `release velocity differs by ${delta.toFixed(3)} m/s (reference ${refSpeed.toFixed(2)} m/s, replay ${speed.toFixed(2)} m/s, ${allowed.toFixed(3)} allowed)` });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return differences;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The trace parity suite over a set of traces: for each, replay through the
|
|
322
|
+
* subject and compare with the reference. A case fails with every difference
|
|
323
|
+
* listed. A synthetic trace's case names itself as such: it proves the
|
|
324
|
+
* pipeline runs on that platform, not parity with the web.
|
|
325
|
+
*/
|
|
326
|
+
export function traceParityCases(traces, tolerances = {}) {
|
|
327
|
+
return traces.map((trace) => {
|
|
328
|
+
const label = `${trace.meta.mechanic} with ${trace.meta.input} on ${trace.meta.device}${trace.meta.synthetic ? " (synthetic: proves the pipeline, not parity)" : ""}`;
|
|
329
|
+
return {
|
|
330
|
+
name: `trace parity: ${label}`,
|
|
331
|
+
trace,
|
|
332
|
+
run(subject) {
|
|
333
|
+
if (trace.version !== TRACE_VERSION)
|
|
334
|
+
throw new Error(`[trace parity] "${label}" is ${String(trace.version)}, this suite reads ${TRACE_VERSION}`);
|
|
335
|
+
const differences = compareOutcomes(referenceOutcome(trace), replayTrace(trace, subject), tolerances);
|
|
336
|
+
if (differences.length > 0) {
|
|
337
|
+
throw new Error(`[trace parity] "${label}" differs from the reference:\n` + differences.map((d) => ` - ${d.interactableId}: ${d.detail}`).join("\n"));
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/** Check a parsed JSON value is a trace this module can replay. */
|
|
344
|
+
export function isInteractionTrace(value) {
|
|
345
|
+
if (typeof value !== "object" || value === null)
|
|
346
|
+
return false;
|
|
347
|
+
const trace = value;
|
|
348
|
+
return trace.version === TRACE_VERSION && Array.isArray(trace.frames) && Array.isArray(trace.targets) && typeof trace.meta === "object" && trace.meta !== null;
|
|
349
|
+
}
|
|
350
|
+
//# sourceMappingURL=trace.js.map
|