@realitycollective/webxr-interactions 0.1.1-preview.2 → 0.1.1-preview.3

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/dist/gaze.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"gaze.js","sourceRoot":"","sources":["../src/gaze.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAkBH,MAAM,CAAC,MAAM,cAAc,GAA0B;IACnD,WAAW,EAAE,GAAG;IAChB,WAAW,EAAE,GAAG;IAChB,UAAU,EAAE,GAAG;CAChB,CAAC;AAEF,MAAM,UAAU,kBAAkB,CAChC,MAAyC,EACzC,WAAkC,cAAc;IAEhD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IACrC,OAAO;QACL,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,QAAQ,CAAC,WAAW;QACvD,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,QAAQ,CAAC,WAAW;QACvD,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU;KACrD,CAAC;AACJ,CAAC;AAWD,MAAM,OAAO,UAAU;IACb,QAAQ,GAAG,CAAC,CAAC;IACb,YAAY,GAAG,CAAC,CAAC;IACjB,KAAK,GAAG,IAAI,CAAC;IAErB,MAAM,CAAC,OAAgB,EAAE,EAAU,EAAE,MAA6B;QAChE,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC7B,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,GAAG,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;QACvE,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CACtB,CAAC,EACD,IAAI,CAAC,QAAQ,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC,WAAW,CAC/D,CAAC;QACJ,CAAC;QAED,IAAI,KAAK,GAAG,KAAK,CAAC;QAClB,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC;YACrC,KAAK,GAAG,IAAI,CAAC;YACb,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;YACnB,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;QACpB,CAAC;aAAM,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YAC7D,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;QAED,MAAM,OAAO,GACX,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,IAAI;YACnD,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,CAAC,CAAC;QACxC,IAAI,OAAO;YAAE,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC/C,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACrD,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;CACF","sourcesContent":["/**\n * Gaze support - first-class, per the design decision that gaze serves two\n * roles:\n *\n * 1. **Combination gating** (`gaze.required`): input on an interactable only\n * counts while the viewer is looking at it.\n * 2. **Accessibility** (`gaze.dwell`): looking at an interactable fills a\n * dwell meter; a full meter fires a synthesized press - no hands needed.\n * Consumers render the meter from `dwellProgress` events.\n *\n * The dwell state machine is pure and headless-tested. The runtime feeds it\n * hover booleans; it never sees the engine.\n */\n\nexport interface DwellConfig {\n /** Seconds of sustained gaze to fire. */\n holdSeconds?: number;\n /** Drain speed multiplier when looking away. */\n decayFactor?: number;\n /** After firing, progress must fall below this before it can re-arm. */\n rearmBelow?: number;\n}\n\nexport interface GazeConfig {\n /** Non-gaze input on this interactable only counts while gazed at. */\n required?: boolean;\n /** Enable dwell-to-press. `true` uses defaults. */\n dwell?: boolean | DwellConfig;\n}\n\nexport const DWELL_DEFAULTS: Required<DwellConfig> = {\n holdSeconds: 1.8,\n decayFactor: 2.5,\n rearmBelow: 0.1,\n};\n\nexport function resolveDwellConfig(\n config: boolean | DwellConfig | undefined,\n defaults: Required<DwellConfig> = DWELL_DEFAULTS,\n): Required<DwellConfig> | null {\n if (!config) return null;\n if (config === true) return defaults;\n return {\n holdSeconds: config.holdSeconds ?? defaults.holdSeconds,\n decayFactor: config.decayFactor ?? defaults.decayFactor,\n rearmBelow: config.rearmBelow ?? defaults.rearmBelow,\n };\n}\n\nexport interface DwellTick {\n /** Dwell meter 0..1. */\n progress: number;\n /** Progress moved enough to be worth re-rendering (≥ 1%). */\n changed: boolean;\n /** The meter just completed - fire the synthesized press. */\n fired: boolean;\n}\n\nexport class DwellState {\n private progress = 0;\n private lastReported = 0;\n private armed = true;\n\n update(hovered: boolean, dt: number, config: Required<DwellConfig>): DwellTick {\n const before = this.progress;\n if (hovered) {\n this.progress = Math.min(1, this.progress + dt / config.holdSeconds);\n } else {\n this.progress = Math.max(\n 0,\n this.progress - (dt * config.decayFactor) / config.holdSeconds,\n );\n }\n\n let fired = false;\n if (this.armed && this.progress >= 1) {\n fired = true;\n this.armed = false;\n this.progress = 0;\n } else if (!this.armed && this.progress <= config.rearmBelow) {\n this.armed = true;\n }\n\n const changed =\n Math.abs(this.progress - this.lastReported) >= 0.01 ||\n (this.progress === 0 && before !== 0);\n if (changed) this.lastReported = this.progress;\n return { progress: this.progress, changed, fired };\n }\n\n getProgress(): number {\n return this.progress;\n }\n}\n"]}
1
+ {"version":3,"file":"gaze.js","sourceRoot":"","sources":["../src/gaze.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Gaze support - first-class, per the design decision that gaze serves two\n * roles:\n *\n * 1. **Combination gating** (`gaze.required`): input on an interactable only\n * counts while the viewer is looking at it.\n * 2. **Accessibility** (`gaze.dwell`): looking at an interactable fills a\n * dwell meter; a full meter fires a synthesized press - no hands needed.\n * Consumers render the meter from `dwellProgress` events.\n *\n * The dwell state machine is pure and headless-tested. The runtime feeds it\n * hover booleans; it never sees the engine.\n */\n\nimport type { RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type { InteractableHit } from \"./ports.js\";\n\nexport interface DwellConfig {\n /** Seconds of sustained gaze to fire. */\n holdSeconds?: number;\n /** Drain speed multiplier when looking away. */\n decayFactor?: number;\n /** After firing, progress must fall below this before it can re-arm. */\n rearmBelow?: number;\n}\n\nexport interface GazeConfig {\n /** Non-gaze input on this interactable only counts while gazed at. */\n required?: boolean;\n /** Enable dwell-to-press. `true` uses defaults. */\n dwell?: boolean | DwellConfig;\n}\n\nexport const DWELL_DEFAULTS: Required<DwellConfig> = {\n holdSeconds: 1.8,\n decayFactor: 2.5,\n rearmBelow: 0.1,\n};\n\nexport function resolveDwellConfig(\n config: boolean | DwellConfig | undefined,\n defaults: Required<DwellConfig> = DWELL_DEFAULTS,\n): Required<DwellConfig> | null {\n if (!config) return null;\n if (config === true) return defaults;\n return {\n holdSeconds: config.holdSeconds ?? defaults.holdSeconds,\n decayFactor: config.decayFactor ?? defaults.decayFactor,\n rearmBelow: config.rearmBelow ?? defaults.rearmBelow,\n };\n}\n\nexport interface DwellTick {\n /** Dwell meter 0..1. */\n progress: number;\n /** Progress moved enough to be worth re-rendering (≥ 1%). */\n changed: boolean;\n /** The meter just completed - fire the synthesized press. */\n fired: boolean;\n}\n\nexport class DwellState {\n private progress = 0;\n private lastReported = 0;\n private armed = true;\n\n update(hovered: boolean, dt: number, config: Required<DwellConfig>): DwellTick {\n const before = this.progress;\n if (hovered) {\n this.progress = Math.min(1, this.progress + dt / config.holdSeconds);\n } else {\n this.progress = Math.max(\n 0,\n this.progress - (dt * config.decayFactor) / config.holdSeconds,\n );\n }\n\n let fired = false;\n if (this.armed && this.progress >= 1) {\n fired = true;\n this.armed = false;\n this.progress = 0;\n } else if (!this.armed && this.progress <= config.rearmBelow) {\n this.armed = true;\n }\n\n const changed =\n Math.abs(this.progress - this.lastReported) >= 0.01 ||\n (this.progress === 0 && before !== 0);\n if (changed) this.lastReported = this.progress;\n return { progress: this.progress, changed, fired };\n }\n\n getProgress(): number {\n return this.progress;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Eye gaze: the targeting half of the rule `@realitycollective/webxr-input`'s\n// `eye-gaze.ts` states. IWSDK's `GazeConecaster` picks the frame's best\n// in-cone candidate and then holds a time-weighted vote over the dwell\n// window, so a flick across a neighbour does not steal hover.\n// ---------------------------------------------------------------------------\n\n\n/** Comparer tolerance: angular distances within this are \"the same\", and distance decides. IWSDK `ANGLE_EPSILON`. */\nexport const GAZE_CONE_ANGLE_EPSILON = 0.5 * (Math.PI / 180);\n\n/** A cone hit: {@link InteractableHit} plus how far off the gaze axis the target's silhouette sat, radians (0 when the ray itself hit). */\nexport interface GazeConeHit extends InteractableHit {\n angularDistance: number;\n}\n\n/** A sphere target, as the approximate hit testers and the native host model one. */\nexport interface SphereTarget {\n id: string;\n center: Vec3Tuple;\n radius: number;\n}\n\n/**\n * IWSDK's `GazeConecaster.findFrameBest` over sphere targets: the best\n * target inside a cone of `halfAngle` radians about `ray`, no farther than\n * `maxLength`. A sphere the ray passes through is hit where the ray enters\n * it, with an angular distance of 0. Otherwise the angle from the ray to the\n * sphere's silhouette (the angle to its centre less the angle it subtends)\n * must be within the cone, and the point aimed at is the point of the sphere\n * nearest the ray. Closer by angle wins; within {@link GAZE_CONE_ANGLE_EPSILON}\n * the nearer target wins. A sphere the origin is inside counts as on axis.\n */\nexport function coneHitForSpheres(\n ray: RayTuple,\n targets: Iterable<SphereTarget>,\n halfAngle: number,\n maxLength: number,\n): GazeConeHit | null {\n const [ox, oy, oz] = ray.origin;\n const [dx, dy, dz] = ray.direction;\n let best: GazeConeHit | null = null;\n for (const target of targets) {\n const [cx, cy, cz] = target.center;\n const r = target.radius;\n const vx = cx - ox;\n const vy = cy - oy;\n const vz = cz - oz;\n const dist = Math.hypot(vx, vy, vz);\n if (dist - r > maxLength) continue;\n let hit: GazeConeHit;\n if (dist <= r) {\n // The origin is inside the sphere: fully on axis, aimed at the centre.\n hit = { interactableId: target.id, distance: dist, point: [cx, cy, cz], angularDistance: 0 };\n } else {\n const t = vx * dx + vy * dy + vz * dz;\n const missSq = Math.max(0, dist * dist - t * t);\n const miss = Math.sqrt(missSq);\n if (t > 0 && miss <= r) {\n // The ray enters the sphere: the raw ray wins, at the entry point.\n const entry = t - Math.sqrt(r * r - missSq);\n if (entry > maxLength) continue;\n hit = {\n interactableId: target.id,\n distance: entry,\n point: [ox + dx * entry, oy + dy * entry, oz + dz * entry],\n angularDistance: 0,\n };\n } else {\n const toCentre = Math.atan2(miss, t);\n const angular = Math.max(0, toCentre - Math.asin(Math.min(1, r / dist)));\n if (angular > halfAngle) continue;\n // The point of the sphere nearest the ray: from the centre toward the\n // ray's closest point (never behind the origin), out to the surface.\n const tc = Math.max(0, t);\n let nx = ox + dx * tc - cx;\n let ny = oy + dy * tc - cy;\n let nz = oz + dz * tc - cz;\n const nl = Math.hypot(nx, ny, nz);\n if (nl > 1e-9) {\n nx /= nl;\n ny /= nl;\n nz /= nl;\n } else {\n nx = -vx / dist;\n ny = -vy / dist;\n nz = -vz / dist;\n }\n const point: Vec3Tuple = [cx + nx * r, cy + ny * r, cz + nz * r];\n const distance = Math.hypot(point[0] - ox, point[1] - oy, point[2] - oz);\n if (distance > maxLength) continue;\n hit = { interactableId: target.id, distance, point, angularDistance: angular };\n }\n }\n if (best === null || compareConeHits(hit, best) < 0) best = hit;\n }\n return best;\n}\n\n/** IWSDK's default comparer: closer by angle, then closer by distance. Negative when `a` is better. */\nexport function compareConeHits(a: GazeConeHit, b: GazeConeHit): number {\n const dAngle = a.angularDistance - b.angularDistance;\n if (Math.abs(dAngle) > GAZE_CONE_ANGLE_EPSILON) return dAngle;\n return a.distance - b.distance;\n}\n\n/**\n * IWSDK's dwell consensus (`GazeConecaster.recordDwell` / `pruneDwell` /\n * `electDwellWinner` / `resolveDwellWinner`): each frame's best candidate is\n * recorded with its `dt`; samples older than the window are dropped; the\n * candidate with the largest cumulative `dt` inside the window wins. A winner\n * that is no longer available (unregistered, disabled, hidden) is struck\n * from the record and the next is elected. A window of 0 keeps only this\n * frame's sample, so the frame's best wins outright.\n */\nexport class GazeConsensus {\n private readonly samples: Array<{ id: string; time: number; dt: number }> = [];\n private time = 0;\n\n constructor(public windowSeconds: number = 0.15) {}\n\n /** Record this frame's best (or none), advance the window and elect. */\n update(frameBestId: string | null, dt: number, available: (id: string) => boolean): string | null {\n this.time += dt;\n if (frameBestId !== null) this.samples.push({ id: frameBestId, time: this.time, dt });\n this.prune();\n for (;;) {\n const elected = this.elect();\n if (elected === null) return null;\n if (available(elected)) return elected;\n this.remove(elected);\n }\n }\n\n /** Advance the window with nothing recorded (a frame gaze was suppressed or had no pose). */\n skip(dt: number): void {\n this.time += dt;\n }\n\n reset(): void {\n this.samples.length = 0;\n }\n\n private prune(): void {\n const cutoff = this.time - this.windowSeconds;\n let drop = 0;\n while (drop < this.samples.length && this.samples[drop]!.time < cutoff) drop += 1;\n if (drop > 0) this.samples.splice(0, drop);\n }\n\n private elect(): string | null {\n let bestId: string | null = null;\n let bestDt = -1;\n const sums = new Map<string, number>();\n for (const sample of this.samples) sums.set(sample.id, (sums.get(sample.id) ?? 0) + sample.dt);\n // First recorded wins a tie, as IWSDK's scan from the ring head does.\n for (const sample of this.samples) {\n const sum = sums.get(sample.id)!;\n if (sum > bestDt) {\n bestDt = sum;\n bestId = sample.id;\n }\n }\n return bestId;\n }\n\n private remove(id: string): void {\n for (let i = this.samples.length - 1; i >= 0; i--) {\n if (this.samples[i]!.id === id) this.samples.splice(i, 1);\n }\n }\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -25,6 +25,11 @@ export * from "./descriptor.js";
25
25
  export * from "./velocity-tracker.js";
26
26
  export * from "./runtime.js";
27
27
  export * from "./pointer-bridge.js";
28
+ export * from "./near-pointer.js";
29
+ export * from "./near-pointer-contract-cases.js";
30
+ export * from "./physics.js";
31
+ export * from "./physics-contract-cases.js";
32
+ export * from "./memory-physics.js";
28
33
  export * from "./behaviours/behaviour.js";
29
34
  export * from "./behaviours/press.js";
30
35
  export * from "./behaviours/pulse.js";
package/dist/index.js CHANGED
@@ -25,6 +25,11 @@ export * from "./descriptor.js";
25
25
  export * from "./velocity-tracker.js";
26
26
  export * from "./runtime.js";
27
27
  export * from "./pointer-bridge.js";
28
+ export * from "./near-pointer.js";
29
+ export * from "./near-pointer-contract-cases.js";
30
+ export * from "./physics.js";
31
+ export * from "./physics-contract-cases.js";
32
+ export * from "./memory-physics.js";
28
33
  export * from "./behaviours/behaviour.js";
29
34
  export * from "./behaviours/press.js";
30
35
  export * from "./behaviours/pulse.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,cAAc,gCAAgC,CAAC;AAE/C,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AAEpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC","sourcesContent":["/**\n * @realitycollective/webxr-interactions - the engine-free interaction core.\n *\n * Layers (each strictly on the one below):\n * - behaviours (press, pulse, hinge, dial, slide, grab, tossScore) -\n * pure state machines driven by `update(dt)`;\n * - the runtime/binder - targeting, press/grab transitions, gaze gating\n * + dwell, capability negotiation, events, feedback intents;\n * - the shared input contracts, re-exported from\n * `@realitycollective/webxr-input`.\n *\n * Pair with an engine adapter: `@realitycollective/threejs-interactions`\n * (standalone WebXR + three.js), `@realitycollective/iwsdk-interactions`\n * (Meta IWSDK) or `@realitycollective/xrblocks-interactions` (Google XR\n * Blocks, experimental).\n */\nexport * from \"@realitycollective/webxr-input\";\n\nexport * from \"./math.js\";\nexport * from \"./events.js\";\nexport * from \"./feedback.js\";\nexport * from \"./ports.js\";\nexport * from \"./contract-cases.js\";\nexport * from \"./gaze.js\";\nexport * from \"./descriptor.js\";\nexport * from \"./velocity-tracker.js\";\nexport * from \"./runtime.js\";\nexport * from \"./pointer-bridge.js\";\n\nexport * from \"./behaviours/behaviour.js\";\nexport * from \"./behaviours/press.js\";\nexport * from \"./behaviours/pulse.js\";\nexport * from \"./behaviours/hinge.js\";\nexport * from \"./behaviours/dial.js\";\nexport * from \"./behaviours/slide.js\";\nexport * from \"./behaviours/grab.js\";\nexport * from \"./behaviours/toss-score.js\";\nexport * from \"./behaviours/factory.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,cAAc,gCAAgC,CAAC;AAE/C,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kCAAkC,CAAC;AACjD,cAAc,cAAc,CAAC;AAC7B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AAEpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC","sourcesContent":["/**\n * @realitycollective/webxr-interactions - the engine-free interaction core.\n *\n * Layers (each strictly on the one below):\n * - behaviours (press, pulse, hinge, dial, slide, grab, tossScore) -\n * pure state machines driven by `update(dt)`;\n * - the runtime/binder - targeting, press/grab transitions, gaze gating\n * + dwell, capability negotiation, events, feedback intents;\n * - the shared input contracts, re-exported from\n * `@realitycollective/webxr-input`.\n *\n * Pair with an engine adapter: `@realitycollective/threejs-interactions`\n * (standalone WebXR + three.js), `@realitycollective/iwsdk-interactions`\n * (Meta IWSDK) or `@realitycollective/xrblocks-interactions` (Google XR\n * Blocks, experimental).\n */\nexport * from \"@realitycollective/webxr-input\";\n\nexport * from \"./math.js\";\nexport * from \"./events.js\";\nexport * from \"./feedback.js\";\nexport * from \"./ports.js\";\nexport * from \"./contract-cases.js\";\nexport * from \"./gaze.js\";\nexport * from \"./descriptor.js\";\nexport * from \"./velocity-tracker.js\";\nexport * from \"./runtime.js\";\nexport * from \"./pointer-bridge.js\";\nexport * from \"./near-pointer.js\";\nexport * from \"./near-pointer-contract-cases.js\";\nexport * from \"./physics.js\";\nexport * from \"./physics-contract-cases.js\";\nexport * from \"./memory-physics.js\";\n\nexport * from \"./behaviours/behaviour.js\";\nexport * from \"./behaviours/press.js\";\nexport * from \"./behaviours/pulse.js\";\nexport * from \"./behaviours/hinge.js\";\nexport * from \"./behaviours/dial.js\";\nexport * from \"./behaviours/slide.js\";\nexport * from \"./behaviours/grab.js\";\nexport * from \"./behaviours/toss-score.js\";\nexport * from \"./behaviours/factory.js\";\n"]}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * MemoryPhysicsFacility - the family's in-memory reference physics: an
3
+ * engine-free `PhysicsFacility` that runs headlessly with the core defaults,
4
+ * so the contract suite has a correct subject to prove itself against and a
5
+ * new binding has a reference to compare with (the same idea as
6
+ * `MemoryInputProvider` in `@realitycollective/webxr-input`).
7
+ *
8
+ * It simulates exactly what the contract cases need and states its limits:
9
+ * gravity (with per-body factor and damping) integrated per step, spheres
10
+ * against axis-aligned static or kinematic boxes with restitution combined
11
+ * as the average of the two bodies (Rapier's default rule), and nothing
12
+ * else: no sphere-to-sphere contact, no friction dynamics, no rotation of
13
+ * boxes. It is not a game engine; a platform binding names a real one.
14
+ */
15
+ import type { PoseTuple, Vec3Tuple } from "@realitycollective/webxr-input";
16
+ import type { HoldRelease } from "./ports.js";
17
+ import { type PhysicsBodySpec, type PhysicsBodyState, type PhysicsFacility, type PhysicsShapeSpec, type PhysicsVelocity } from "./physics.js";
18
+ export declare class MemoryPhysicsFacility implements PhysicsFacility {
19
+ readonly engine: string;
20
+ private gravity;
21
+ private readonly bodies;
22
+ getGravity(): Vec3Tuple;
23
+ setGravity(gravity: Vec3Tuple): void;
24
+ addBody(id: string, pose: PoseTuple, body?: PhysicsBodySpec, shape?: PhysicsShapeSpec): void;
25
+ removeBody(id: string): void;
26
+ hasBody(id: string): boolean;
27
+ setBodyState(id: string, state: PhysicsBodyState): void;
28
+ getBodyState(id: string): PhysicsBodyState;
29
+ getBodyPose(id: string): PoseTuple;
30
+ setBodyPose(id: string, pose: PoseTuple): void;
31
+ getVelocity(id: string): PhysicsVelocity;
32
+ setVelocity(id: string, velocity: PhysicsVelocity): void;
33
+ suspend(id: string): void;
34
+ resume(id: string, release: HoldRelease): void;
35
+ isSuspended(id: string): boolean;
36
+ step(dtSeconds: number): void;
37
+ dispose(): void;
38
+ private body;
39
+ /** A moving sphere against every non-dynamic axis-aligned box: push out along the least-penetrated axis and reflect the normal velocity. */
40
+ private collideSphere;
41
+ }
@@ -0,0 +1,168 @@
1
+ import { PHYSICS_DEFAULTS, missingBody, resolvePhysicsBody, resolvePhysicsShape, } from "./physics.js";
2
+ function copy3(v) {
3
+ return [v[0], v[1], v[2]];
4
+ }
5
+ export class MemoryPhysicsFacility {
6
+ engine = "memory";
7
+ gravity = copy3(PHYSICS_DEFAULTS.gravity);
8
+ bodies = new Map();
9
+ getGravity() {
10
+ return copy3(this.gravity);
11
+ }
12
+ setGravity(gravity) {
13
+ this.gravity = copy3(gravity);
14
+ }
15
+ addBody(id, pose, body = {}, shape = {}) {
16
+ const spec = resolvePhysicsBody(body);
17
+ this.bodies.set(id, {
18
+ state: spec.state,
19
+ spec,
20
+ shape: resolvePhysicsShape(shape),
21
+ position: copy3(pose.position),
22
+ quaternion: [pose.quaternion[0], pose.quaternion[1], pose.quaternion[2], pose.quaternion[3]],
23
+ linear: [0, 0, 0],
24
+ angular: [0, 0, 0],
25
+ suspended: false,
26
+ });
27
+ }
28
+ removeBody(id) {
29
+ this.bodies.delete(id);
30
+ }
31
+ hasBody(id) {
32
+ return this.bodies.has(id);
33
+ }
34
+ setBodyState(id, state) {
35
+ const body = this.body(id);
36
+ body.state = state;
37
+ if (state !== "dynamic") {
38
+ body.linear = [0, 0, 0];
39
+ body.angular = [0, 0, 0];
40
+ }
41
+ }
42
+ getBodyState(id) {
43
+ return this.body(id).state;
44
+ }
45
+ getBodyPose(id) {
46
+ const body = this.body(id);
47
+ return { position: copy3(body.position), quaternion: [...body.quaternion] };
48
+ }
49
+ setBodyPose(id, pose) {
50
+ const body = this.body(id);
51
+ body.position = copy3(pose.position);
52
+ body.quaternion = [pose.quaternion[0], pose.quaternion[1], pose.quaternion[2], pose.quaternion[3]];
53
+ // A teleport rests the body; a write while held carries nothing either.
54
+ body.linear = [0, 0, 0];
55
+ body.angular = [0, 0, 0];
56
+ }
57
+ getVelocity(id) {
58
+ const body = this.body(id);
59
+ return { linear: copy3(body.linear), angular: copy3(body.angular) };
60
+ }
61
+ setVelocity(id, velocity) {
62
+ const body = this.body(id);
63
+ body.linear = copy3(velocity.linear);
64
+ body.angular = copy3(velocity.angular);
65
+ }
66
+ suspend(id) {
67
+ const body = this.body(id);
68
+ if (body.suspended)
69
+ return;
70
+ body.suspended = true;
71
+ body.linear = [0, 0, 0];
72
+ body.angular = [0, 0, 0];
73
+ }
74
+ resume(id, release) {
75
+ const body = this.body(id);
76
+ if (!body.suspended)
77
+ return;
78
+ body.suspended = false;
79
+ body.linear = copy3(release.linearVelocity);
80
+ body.angular = copy3(release.angularVelocity);
81
+ }
82
+ isSuspended(id) {
83
+ return this.body(id).suspended;
84
+ }
85
+ step(dtSeconds) {
86
+ if (dtSeconds <= 0)
87
+ return;
88
+ for (const body of this.bodies.values()) {
89
+ if (body.state !== "dynamic" || body.suspended)
90
+ continue;
91
+ const g = body.spec.gravityFactor;
92
+ body.linear[0] += this.gravity[0] * g * dtSeconds;
93
+ body.linear[1] += this.gravity[1] * g * dtSeconds;
94
+ body.linear[2] += this.gravity[2] * g * dtSeconds;
95
+ const linearKeep = Math.max(0, 1 - body.spec.linearDamping * dtSeconds);
96
+ const angularKeep = Math.max(0, 1 - body.spec.angularDamping * dtSeconds);
97
+ body.linear = [body.linear[0] * linearKeep, body.linear[1] * linearKeep, body.linear[2] * linearKeep];
98
+ body.angular = [body.angular[0] * angularKeep, body.angular[1] * angularKeep, body.angular[2] * angularKeep];
99
+ body.position = [
100
+ body.position[0] + body.linear[0] * dtSeconds,
101
+ body.position[1] + body.linear[1] * dtSeconds,
102
+ body.position[2] + body.linear[2] * dtSeconds,
103
+ ];
104
+ body.quaternion = integrateRotation(body.quaternion, body.angular, dtSeconds);
105
+ if (body.shape.kind === "sphere")
106
+ this.collideSphere(body);
107
+ }
108
+ }
109
+ dispose() {
110
+ this.bodies.clear();
111
+ }
112
+ body(id) {
113
+ const body = this.bodies.get(id);
114
+ if (!body)
115
+ throw missingBody(id);
116
+ return body;
117
+ }
118
+ /** A moving sphere against every non-dynamic axis-aligned box: push out along the least-penetrated axis and reflect the normal velocity. */
119
+ collideSphere(sphere) {
120
+ const radius = sphere.shape.dimensions[0];
121
+ for (const other of this.bodies.values()) {
122
+ if (other === sphere || other.shape.kind !== "box" || other.state === "dynamic")
123
+ continue;
124
+ const half = [other.shape.dimensions[0] / 2, other.shape.dimensions[1] / 2, other.shape.dimensions[2] / 2];
125
+ let bestAxis = -1;
126
+ let bestDepth = Number.POSITIVE_INFINITY;
127
+ let bestSign = 1;
128
+ for (let axis = 0; axis < 3; axis++) {
129
+ const delta = sphere.position[axis] - other.position[axis];
130
+ const depth = half[axis] + radius - Math.abs(delta);
131
+ if (depth <= 0) {
132
+ bestAxis = -1;
133
+ break;
134
+ }
135
+ if (depth < bestDepth) {
136
+ bestDepth = depth;
137
+ bestAxis = axis;
138
+ bestSign = delta >= 0 ? 1 : -1;
139
+ }
140
+ }
141
+ if (bestAxis < 0)
142
+ continue;
143
+ sphere.position[bestAxis] = sphere.position[bestAxis] + bestSign * bestDepth;
144
+ const normalVelocity = sphere.linear[bestAxis] * bestSign;
145
+ if (normalVelocity < 0) {
146
+ const restitution = (sphere.shape.restitution + other.shape.restitution) / 2;
147
+ sphere.linear[bestAxis] = -normalVelocity * restitution * bestSign;
148
+ }
149
+ }
150
+ }
151
+ }
152
+ /** Advance a quaternion by an angular velocity (axis scaled by rad/s) over dt, first order. */
153
+ function integrateRotation(q, angular, dt) {
154
+ const angle = Math.hypot(angular[0], angular[1], angular[2]) * dt;
155
+ if (angle < 1e-9)
156
+ return q;
157
+ const half = angle / 2;
158
+ const s = Math.sin(half) / (angle / dt) * dt;
159
+ const d = [angular[0] * s, angular[1] * s, angular[2] * s, Math.cos(half)];
160
+ // d * q
161
+ return [
162
+ d[3] * q[0] + d[0] * q[3] + d[1] * q[2] - d[2] * q[1],
163
+ d[3] * q[1] - d[0] * q[2] + d[1] * q[3] + d[2] * q[0],
164
+ d[3] * q[2] + d[0] * q[1] - d[1] * q[0] + d[2] * q[3],
165
+ d[3] * q[3] - d[0] * q[0] - d[1] * q[1] - d[2] * q[2],
166
+ ];
167
+ }
168
+ //# sourceMappingURL=memory-physics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-physics.js","sourceRoot":"","sources":["../src/memory-physics.ts"],"names":[],"mappings":"AAgBA,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,mBAAmB,GAMpB,MAAM,cAAc,CAAC;AAatB,SAAS,KAAK,CAAC,CAAsB;IACnC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,OAAO,qBAAqB;IACvB,MAAM,GAAW,QAAQ,CAAC;IAC3B,OAAO,GAAc,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,GAAG,IAAI,GAAG,EAAgB,CAAC;IAElD,UAAU;QACR,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAED,UAAU,CAAC,OAAkB;QAC3B,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IAChC,CAAC;IAED,OAAO,CAAC,EAAU,EAAE,IAAe,EAAE,OAAwB,EAAE,EAAE,QAA0B,EAAE;QAC3F,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE;YAClB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,IAAI;YACJ,KAAK,EAAE,mBAAmB,CAAC,KAAK,CAAC;YACjC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC;YAC9B,UAAU,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YAC5F,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YACjB,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YAClB,SAAS,EAAE,KAAK;SACjB,CAAC,CAAC;IACL,CAAC;IAED,UAAU,CAAC,EAAU;QACnB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,CAAC,EAAU;QAChB,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC7B,CAAC;IAED,YAAY,CAAC,EAAU,EAAE,KAAuB;QAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YACxB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,YAAY,CAAC,EAAU;QACrB,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC;IAC7B,CAAC;IAED,WAAW,CAAC,EAAU;QACpB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3B,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,IAAI,CAAC,UAAU,CAAc,EAAE,CAAC;IAC3F,CAAC;IAED,WAAW,CAAC,EAAU,EAAE,IAAe;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3B,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,UAAU,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACnG,wEAAwE;QACxE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3B,CAAC;IAED,WAAW,CAAC,EAAU;QACpB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3B,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;IACtE,CAAC;IAED,WAAW,CAAC,EAAU,EAAE,QAAyB;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACzC,CAAC;IAED,OAAO,CAAC,EAAU;QAChB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3B,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,CAAC,EAAU,EAAE,OAAoB;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE,OAAO;QAC5B,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAChD,CAAC;IAED,WAAW,CAAC,EAAU;QACpB,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACjC,CAAC;IAED,IAAI,CAAC,SAAiB;QACpB,IAAI,SAAS,IAAI,CAAC;YAAE,OAAO;QAC3B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;YACxC,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,SAAS;gBAAE,SAAS;YACzD,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC;YAClC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;YAClD,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;YAClD,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;YAClD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;YACxE,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;YAC1E,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC;YACtG,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC;YAC7G,IAAI,CAAC,QAAQ,GAAG;gBACd,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,SAAS;gBAC7C,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,SAAS;gBAC7C,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,SAAS;aAC9C,CAAC;YACF,IAAI,CAAC,UAAU,GAAG,iBAAiB,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;YAC9E,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ;gBAAE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED,OAAO;QACL,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;IAEO,IAAI,CAAC,EAAU;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI;YAAE,MAAM,WAAW,CAAC,EAAE,CAAC,CAAC;QACjC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,4IAA4I;IACpI,aAAa,CAAC,MAAY;QAChC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC1C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;YACzC,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;gBAAE,SAAS;YAC1F,MAAM,IAAI,GAAc,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACtH,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC;YAClB,IAAI,SAAS,GAAG,MAAM,CAAC,iBAAiB,CAAC;YACzC,IAAI,QAAQ,GAAG,CAAC,CAAC;YACjB,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;gBACpC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAE,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAE,CAAC;gBAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAE,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;gBACrD,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;oBACf,QAAQ,GAAG,CAAC,CAAC,CAAC;oBACd,MAAM;gBACR,CAAC;gBACD,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;oBACtB,SAAS,GAAG,KAAK,CAAC;oBAClB,QAAQ,GAAG,IAAI,CAAC;oBAChB,QAAQ,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACjC,CAAC;YACH,CAAC;YACD,IAAI,QAAQ,GAAG,CAAC;gBAAE,SAAS;YAC3B,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAE,GAAG,QAAQ,GAAG,SAAS,CAAC;YAC9E,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAE,GAAG,QAAQ,CAAC;YAC3D,IAAI,cAAc,GAAG,CAAC,EAAE,CAAC;gBACvB,MAAM,WAAW,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;gBAC7E,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAc,GAAG,WAAW,GAAG,QAAQ,CAAC;YACrE,CAAC;QACH,CAAC;IACH,CAAC;CACF;AAED,+FAA+F;AAC/F,SAAS,iBAAiB,CAAC,CAAY,EAAE,OAAkB,EAAE,EAAU;IACrE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IAClE,IAAI,KAAK,GAAG,IAAI;QAAE,OAAO,CAAC,CAAC;IAC3B,MAAM,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC;IACvB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;IAC7C,MAAM,CAAC,GAAc,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IACtF,QAAQ;IACR,OAAO;QACL,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KACtD,CAAC;AACJ,CAAC","sourcesContent":["/**\n * MemoryPhysicsFacility - the family's in-memory reference physics: an\n * engine-free `PhysicsFacility` that runs headlessly with the core defaults,\n * so the contract suite has a correct subject to prove itself against and a\n * new binding has a reference to compare with (the same idea as\n * `MemoryInputProvider` in `@realitycollective/webxr-input`).\n *\n * It simulates exactly what the contract cases need and states its limits:\n * gravity (with per-body factor and damping) integrated per step, spheres\n * against axis-aligned static or kinematic boxes with restitution combined\n * as the average of the two bodies (Rapier's default rule), and nothing\n * else: no sphere-to-sphere contact, no friction dynamics, no rotation of\n * boxes. It is not a game engine; a platform binding names a real one.\n */\nimport type { PoseTuple, QuatTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type { HoldRelease } from \"./ports.js\";\nimport {\n PHYSICS_DEFAULTS,\n missingBody,\n resolvePhysicsBody,\n resolvePhysicsShape,\n type PhysicsBodySpec,\n type PhysicsBodyState,\n type PhysicsFacility,\n type PhysicsShapeSpec,\n type PhysicsVelocity,\n} from \"./physics.js\";\n\ninterface Body {\n state: PhysicsBodyState;\n spec: Required<PhysicsBodySpec>;\n shape: Required<PhysicsShapeSpec>;\n position: Vec3Tuple;\n quaternion: QuatTuple;\n linear: Vec3Tuple;\n angular: Vec3Tuple;\n suspended: boolean;\n}\n\nfunction copy3(v: Readonly<Vec3Tuple>): Vec3Tuple {\n return [v[0], v[1], v[2]];\n}\n\nexport class MemoryPhysicsFacility implements PhysicsFacility {\n readonly engine: string = \"memory\";\n private gravity: Vec3Tuple = copy3(PHYSICS_DEFAULTS.gravity);\n private readonly bodies = new Map<string, Body>();\n\n getGravity(): Vec3Tuple {\n return copy3(this.gravity);\n }\n\n setGravity(gravity: Vec3Tuple): void {\n this.gravity = copy3(gravity);\n }\n\n addBody(id: string, pose: PoseTuple, body: PhysicsBodySpec = {}, shape: PhysicsShapeSpec = {}): void {\n const spec = resolvePhysicsBody(body);\n this.bodies.set(id, {\n state: spec.state,\n spec,\n shape: resolvePhysicsShape(shape),\n position: copy3(pose.position),\n quaternion: [pose.quaternion[0], pose.quaternion[1], pose.quaternion[2], pose.quaternion[3]],\n linear: [0, 0, 0],\n angular: [0, 0, 0],\n suspended: false,\n });\n }\n\n removeBody(id: string): void {\n this.bodies.delete(id);\n }\n\n hasBody(id: string): boolean {\n return this.bodies.has(id);\n }\n\n setBodyState(id: string, state: PhysicsBodyState): void {\n const body = this.body(id);\n body.state = state;\n if (state !== \"dynamic\") {\n body.linear = [0, 0, 0];\n body.angular = [0, 0, 0];\n }\n }\n\n getBodyState(id: string): PhysicsBodyState {\n return this.body(id).state;\n }\n\n getBodyPose(id: string): PoseTuple {\n const body = this.body(id);\n return { position: copy3(body.position), quaternion: [...body.quaternion] as QuatTuple };\n }\n\n setBodyPose(id: string, pose: PoseTuple): void {\n const body = this.body(id);\n body.position = copy3(pose.position);\n body.quaternion = [pose.quaternion[0], pose.quaternion[1], pose.quaternion[2], pose.quaternion[3]];\n // A teleport rests the body; a write while held carries nothing either.\n body.linear = [0, 0, 0];\n body.angular = [0, 0, 0];\n }\n\n getVelocity(id: string): PhysicsVelocity {\n const body = this.body(id);\n return { linear: copy3(body.linear), angular: copy3(body.angular) };\n }\n\n setVelocity(id: string, velocity: PhysicsVelocity): void {\n const body = this.body(id);\n body.linear = copy3(velocity.linear);\n body.angular = copy3(velocity.angular);\n }\n\n suspend(id: string): void {\n const body = this.body(id);\n if (body.suspended) return;\n body.suspended = true;\n body.linear = [0, 0, 0];\n body.angular = [0, 0, 0];\n }\n\n resume(id: string, release: HoldRelease): void {\n const body = this.body(id);\n if (!body.suspended) return;\n body.suspended = false;\n body.linear = copy3(release.linearVelocity);\n body.angular = copy3(release.angularVelocity);\n }\n\n isSuspended(id: string): boolean {\n return this.body(id).suspended;\n }\n\n step(dtSeconds: number): void {\n if (dtSeconds <= 0) return;\n for (const body of this.bodies.values()) {\n if (body.state !== \"dynamic\" || body.suspended) continue;\n const g = body.spec.gravityFactor;\n body.linear[0] += this.gravity[0] * g * dtSeconds;\n body.linear[1] += this.gravity[1] * g * dtSeconds;\n body.linear[2] += this.gravity[2] * g * dtSeconds;\n const linearKeep = Math.max(0, 1 - body.spec.linearDamping * dtSeconds);\n const angularKeep = Math.max(0, 1 - body.spec.angularDamping * dtSeconds);\n body.linear = [body.linear[0] * linearKeep, body.linear[1] * linearKeep, body.linear[2] * linearKeep];\n body.angular = [body.angular[0] * angularKeep, body.angular[1] * angularKeep, body.angular[2] * angularKeep];\n body.position = [\n body.position[0] + body.linear[0] * dtSeconds,\n body.position[1] + body.linear[1] * dtSeconds,\n body.position[2] + body.linear[2] * dtSeconds,\n ];\n body.quaternion = integrateRotation(body.quaternion, body.angular, dtSeconds);\n if (body.shape.kind === \"sphere\") this.collideSphere(body);\n }\n }\n\n dispose(): void {\n this.bodies.clear();\n }\n\n private body(id: string): Body {\n const body = this.bodies.get(id);\n if (!body) throw missingBody(id);\n return body;\n }\n\n /** A moving sphere against every non-dynamic axis-aligned box: push out along the least-penetrated axis and reflect the normal velocity. */\n private collideSphere(sphere: Body): void {\n const radius = sphere.shape.dimensions[0];\n for (const other of this.bodies.values()) {\n if (other === sphere || other.shape.kind !== \"box\" || other.state === \"dynamic\") continue;\n const half: Vec3Tuple = [other.shape.dimensions[0] / 2, other.shape.dimensions[1] / 2, other.shape.dimensions[2] / 2];\n let bestAxis = -1;\n let bestDepth = Number.POSITIVE_INFINITY;\n let bestSign = 1;\n for (let axis = 0; axis < 3; axis++) {\n const delta = sphere.position[axis]! - other.position[axis]!;\n const depth = half[axis]! + radius - Math.abs(delta);\n if (depth <= 0) {\n bestAxis = -1;\n break;\n }\n if (depth < bestDepth) {\n bestDepth = depth;\n bestAxis = axis;\n bestSign = delta >= 0 ? 1 : -1;\n }\n }\n if (bestAxis < 0) continue;\n sphere.position[bestAxis] = sphere.position[bestAxis]! + bestSign * bestDepth;\n const normalVelocity = sphere.linear[bestAxis]! * bestSign;\n if (normalVelocity < 0) {\n const restitution = (sphere.shape.restitution + other.shape.restitution) / 2;\n sphere.linear[bestAxis] = -normalVelocity * restitution * bestSign;\n }\n }\n }\n}\n\n/** Advance a quaternion by an angular velocity (axis scaled by rad/s) over dt, first order. */\nfunction integrateRotation(q: QuatTuple, angular: Vec3Tuple, dt: number): QuatTuple {\n const angle = Math.hypot(angular[0], angular[1], angular[2]) * dt;\n if (angle < 1e-9) return q;\n const half = angle / 2;\n const s = Math.sin(half) / (angle / dt) * dt;\n const d: QuatTuple = [angular[0] * s, angular[1] * s, angular[2] * s, Math.cos(half)];\n // d * q\n return [\n d[3] * q[0] + d[0] * q[3] + d[1] * q[2] - d[2] * q[1],\n d[3] * q[1] - d[0] * q[2] + d[1] * q[3] + d[2] * q[0],\n d[3] * q[2] + d[0] * q[1] - d[1] * q[0] + d[2] * q[3],\n d[3] * q[3] - d[0] * q[0] - d[1] * q[1] - d[2] * q[2],\n ];\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import type { HitTesterContractSubject } from "./contract-cases.js";
2
+ /** One check a binding's hit tester must pass under the near-pointer rule. */
3
+ export interface NearPointerContractCase {
4
+ name: string;
5
+ run(subject: HitTesterContractSubject): void;
6
+ }
7
+ /**
8
+ * The shared near-pointer conformance suite. An adapter's test file:
9
+ *
10
+ * ```ts
11
+ * for (const contractCase of nearPointerContractCases()) {
12
+ * it(contractCase.name, () => contractCase.run(makeSubject()));
13
+ * }
14
+ * ```
15
+ */
16
+ export declare function nearPointerContractCases(): readonly NearPointerContractCase[];
@@ -0,0 +1,235 @@
1
+ /**
2
+ * The shared near-pointer conformance suite: the touch press, the grip grab,
3
+ * pointer priority with its selection lock, and the ray and cursor visuals
4
+ * (`near-pointer.ts`), run through a binding's own `HitTester` so every
5
+ * platform proves the same rule over its own scene queries.
6
+ *
7
+ * Runner-free, like `hitTesterContractCases()`: an adapter's test file is a
8
+ * loop over these cases with the same subject factory it already uses for
9
+ * the hit-tester suite. The suite drives an `InteractionRuntime` itself, with
10
+ * a scripted provider, so the only platform code under test is the hit
11
+ * tester and the targets the driver places.
12
+ *
13
+ * Every distance below is metres to the target's SURFACE, the quantity the
14
+ * touch and grab pointers compare (IWSDK's sphere intersector reports the
15
+ * distance from the fingertip to the point on the mesh).
16
+ */
17
+ import { NO_CAPABILITIES, } from "@realitycollective/webxr-input";
18
+ import { NEAR_POINTER_DEFAULTS } from "./near-pointer.js";
19
+ import { InteractionRuntime } from "./runtime.js";
20
+ /**
21
+ * The shared near-pointer conformance suite. An adapter's test file:
22
+ *
23
+ * ```ts
24
+ * for (const contractCase of nearPointerContractCases()) {
25
+ * it(contractCase.name, () => contractCase.run(makeSubject()));
26
+ * }
27
+ * ```
28
+ */
29
+ export function nearPointerContractCases() {
30
+ return NEAR_POINTER_CASES;
31
+ }
32
+ /** A provider whose sources a case sets frame by frame. */
33
+ class ScriptedProvider {
34
+ sources = [];
35
+ capabilities = {
36
+ ...NO_CAPABILITIES,
37
+ rays: true,
38
+ pokes: true,
39
+ grabs: "poseOnly",
40
+ handJoints: true,
41
+ pinch: true,
42
+ headPose: false,
43
+ gaze: false,
44
+ };
45
+ getCapabilities() {
46
+ return this.capabilities;
47
+ }
48
+ onCapabilitiesChanged() {
49
+ return () => undefined;
50
+ }
51
+ onSourcesChanged() {
52
+ return () => undefined;
53
+ }
54
+ sample() {
55
+ return this.sources;
56
+ }
57
+ }
58
+ const CENTRE = [0, 1, -1];
59
+ const RADIUS = 0.1;
60
+ /** A ray from the viewer straight through the target. */
61
+ const RAY_AT = { origin: [0, 1, 0], direction: [0, 0, -1] };
62
+ /** A ray pointing away from everything. */
63
+ const RAY_AWAY = { origin: [0, 1, 0], direction: [0, 0, 1] };
64
+ const FAR = [0, 1, 1];
65
+ /** A point `distance` metres outside the target's surface, on its near side. */
66
+ function outside(distance) {
67
+ return [CENTRE[0], CENTRE[1], CENTRE[2] + RADIUS + distance];
68
+ }
69
+ function hand(frame) {
70
+ const snapshot = {
71
+ id: "right",
72
+ kind: frame.kind ?? "hand",
73
+ handedness: "right",
74
+ select: frame.select ?? 0,
75
+ squeeze: frame.squeeze ?? 0,
76
+ };
77
+ const ray = frame.ray ?? "away";
78
+ if (ray !== "none")
79
+ snapshot.ray = ray === "at" ? { ...RAY_AT } : { ...RAY_AWAY };
80
+ snapshot.indexTip = frame.tip ?? FAR;
81
+ snapshot.gripPose = { position: frame.grip ?? FAR, quaternion: [0, 0, 0, 1] };
82
+ return snapshot;
83
+ }
84
+ function rig(subject, behaviours = [{ kind: "press" }]) {
85
+ subject.driver.place("target", CENTRE, RADIUS);
86
+ const provider = new ScriptedProvider();
87
+ const runtime = new InteractionRuntime({ provider, hitTester: subject.hitTester, velocity: false });
88
+ const events = [];
89
+ const visuals = [];
90
+ runtime.onEvent((event) => events.push(event));
91
+ runtime.onPointerVisuals((list) => {
92
+ visuals.length = 0;
93
+ visuals.push(...list);
94
+ });
95
+ runtime.registerInteractable({ id: "target", behaviours });
96
+ return {
97
+ runtime,
98
+ provider,
99
+ events,
100
+ visuals,
101
+ frame(frame) {
102
+ const before = events.length;
103
+ provider.sources = [hand(frame)];
104
+ runtime.update(1 / 60);
105
+ return events.slice(before).map((event) => event.type);
106
+ },
107
+ };
108
+ }
109
+ function assert(condition, message) {
110
+ if (!condition)
111
+ throw new Error(message);
112
+ }
113
+ function expectActive(r, kind, when) {
114
+ const active = r.runtime.getActivePointer("right");
115
+ assert(active === kind, `${when}: the active pointer must be ${String(kind)}, got ${String(active)}`);
116
+ }
117
+ const D = NEAR_POINTER_DEFAULTS;
118
+ const NEAR_POINTER_CASES = [
119
+ {
120
+ name: "a fingertip approaching from outside hovers at 0.15 m, presses at 0.02 m, releases on the way out and stops hovering beyond 0.2 m",
121
+ run(subject) {
122
+ const r = rig(subject);
123
+ r.frame({ tip: outside(0.3) });
124
+ expectActive(r, null, "at 0.3 m");
125
+ let types = r.frame({ tip: outside(D.touchHoverEnter - 0.01) });
126
+ expectActive(r, "touch", "inside the hover enter distance");
127
+ assert(types.includes("hoverEnter"), "entering the hover band must raise hoverEnter");
128
+ types = r.frame({ tip: outside(0.05) });
129
+ assert(!types.includes("pressStart"), "5 cm out must not press");
130
+ types = r.frame({ tip: outside(D.touchDown) });
131
+ assert(types.includes("pressStart"), "reaching the press distance must raise pressStart");
132
+ assert(r.runtime.getState("target")?.pressed === true, "the target must be pressed");
133
+ types = r.frame({ tip: outside(0.005) });
134
+ assert(!types.includes("pressEnd") && !types.includes("pressStart"), "going deeper must change nothing");
135
+ types = r.frame({ tip: outside(D.touchDown + 0.01) });
136
+ assert(types.includes("pressEnd"), "coming back out past the press distance must release");
137
+ r.frame({ tip: outside(D.touchHoverEnter + 0.02) });
138
+ expectActive(r, "touch", "between enter and exit while hovering (hysteresis)");
139
+ types = r.frame({ tip: outside(D.touchHoverExit + 0.01) });
140
+ expectActive(r, null, "beyond the exit distance");
141
+ assert(types.includes("hoverExit"), "leaving the exit distance must raise hoverExit");
142
+ },
143
+ },
144
+ {
145
+ name: "a fingertip first seen inside the press band never presses until it has been outside it",
146
+ run(subject) {
147
+ const r = rig(subject);
148
+ let types = r.frame({ tip: outside(0.01) });
149
+ assert(types.includes("hoverEnter") && !types.includes("pressStart"), "a fingertip that appears inside the band hovers but must not press");
150
+ types = r.frame({ tip: outside(0.0) });
151
+ assert(!types.includes("pressStart"), "staying inside must not press");
152
+ r.frame({ tip: outside(0.05) });
153
+ types = r.frame({ tip: outside(0.015) });
154
+ assert(types.includes("pressStart"), "coming back in from outside must press");
155
+ },
156
+ },
157
+ {
158
+ name: "a grip within 0.07 m of the surface makes grab the active pointer and a squeeze grabs that target",
159
+ run(subject) {
160
+ const r = rig(subject, [{ kind: "grab" }]);
161
+ r.frame({ grip: outside(D.grabRadius - 0.02), ray: "at" });
162
+ expectActive(r, "grab", "grip inside the grab radius, ray also hitting");
163
+ let types = r.frame({ grip: outside(D.grabRadius - 0.02), ray: "at", squeeze: 1 });
164
+ assert(types.includes("grabStart"), "a squeeze while grab owns the hand must grab");
165
+ types = r.frame({ grip: outside(D.grabRadius - 0.02), ray: "at", squeeze: 0 });
166
+ assert(types.includes("grabEnd"), "releasing the squeeze must end the grab");
167
+ r.frame({ grip: outside(D.grabRadius + 0.01), ray: "away" });
168
+ expectActive(r, null, "grip outside the grab radius with no ray hit");
169
+ },
170
+ },
171
+ {
172
+ name: "touch beats grab beats ray",
173
+ run(subject) {
174
+ const r = rig(subject);
175
+ r.frame({ tip: outside(0.1), grip: outside(0.05), ray: "at" });
176
+ expectActive(r, "touch", "fingertip, grip and ray all on the target");
177
+ r.frame({ grip: outside(0.05), ray: "at" });
178
+ expectActive(r, "grab", "grip and ray on the target");
179
+ r.frame({ ray: "at" });
180
+ expectActive(r, "ray", "only the ray on the target");
181
+ r.frame({ ray: "away" });
182
+ expectActive(r, null, "nothing on the target");
183
+ },
184
+ },
185
+ {
186
+ name: "the ray and its cursor show only while the ray owns the source, and the cursor sits at the active pointer's hit",
187
+ run(subject) {
188
+ const r = rig(subject);
189
+ r.frame({ ray: "at" });
190
+ let v = r.visuals[0];
191
+ assert(v.ray && v.cursor && v.cursorPoint !== null, "with the ray active, the ray and its cursor show");
192
+ assert(Math.abs(v.cursorPoint[2] - (CENTRE[2] + RADIUS)) <= RADIUS + 0.05, "the cursor sits near the ray's hit on the target");
193
+ r.frame({ tip: outside(0.05), ray: "at" });
194
+ v = r.visuals[0];
195
+ assert(!v.ray, "with touch active, the ray is hidden");
196
+ assert(v.cursor && v.cursorPoint !== null, "with touch active, the cursor shows at the touch point");
197
+ assert(Math.abs(v.cursorPoint[2] - (CENTRE[2] + RADIUS)) <= RADIUS + 0.05, "the cursor sits on the surface under the fingertip");
198
+ r.frame({ grip: outside(0.05), ray: "at" });
199
+ v = r.visuals[0];
200
+ assert(!v.ray && v.cursor, "with grab active, the ray is hidden and the cursor shows");
201
+ r.frame({ ray: "away" });
202
+ v = r.visuals[0];
203
+ assert(v.ray && !v.cursor && v.cursorPoint === null, "with nothing active, the ray shows and the cursor does not");
204
+ r.frame({ ray: "none" });
205
+ v = r.visuals[0];
206
+ assert(!v.ray && !v.cursor, "a source with no ray draws no ray");
207
+ },
208
+ },
209
+ {
210
+ name: "a pointer that is pressing keeps the source until it releases",
211
+ run(subject) {
212
+ const r = rig(subject);
213
+ r.frame({ ray: "at" });
214
+ let types = r.frame({ ray: "at", select: 1 });
215
+ assert(types.includes("pressStart"), "a select on the ray target presses it");
216
+ r.frame({ tip: outside(0.05), ray: "at", select: 1 });
217
+ expectActive(r, "ray", "a fingertip entering the band while the ray presses");
218
+ types = r.frame({ tip: outside(0.05), ray: "at", select: 0 });
219
+ assert(types.includes("pressEnd"), "releasing select ends the ray press");
220
+ r.frame({ tip: outside(0.05), ray: "at" });
221
+ expectActive(r, "touch", "once released, touch takes the source");
222
+ },
223
+ },
224
+ {
225
+ name: "a controller's ray origin presses by touch exactly as a fingertip does",
226
+ run(subject) {
227
+ const r = rig(subject);
228
+ r.frame({ kind: "controller", tip: outside(0.1) });
229
+ expectActive(r, "touch", "a controller nose inside the hover band");
230
+ const types = r.frame({ kind: "controller", tip: outside(0.01) });
231
+ assert(types.includes("pressStart"), "a controller nose reaching the press distance presses");
232
+ },
233
+ },
234
+ ];
235
+ //# sourceMappingURL=near-pointer-contract-cases.js.map