@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/CHANGELOG.md +11 -0
- package/README.md +1 -0
- package/dist/contract-cases.js +69 -0
- package/dist/contract-cases.js.map +1 -1
- package/dist/descriptor.d.ts +7 -1
- package/dist/descriptor.js.map +1 -1
- package/dist/gaze.d.ts +50 -0
- package/dist/gaze.js +162 -0
- package/dist/gaze.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/memory-physics.d.ts +41 -0
- package/dist/memory-physics.js +168 -0
- package/dist/memory-physics.js.map +1 -0
- package/dist/near-pointer-contract-cases.d.ts +16 -0
- package/dist/near-pointer-contract-cases.js +235 -0
- package/dist/near-pointer-contract-cases.js.map +1 -0
- package/dist/near-pointer.d.ts +131 -0
- package/dist/near-pointer.js +128 -0
- package/dist/near-pointer.js.map +1 -0
- package/dist/physics-contract-cases.d.ts +11 -0
- package/dist/physics-contract-cases.js +229 -0
- package/dist/physics-contract-cases.js.map +1 -0
- package/dist/physics.d.ts +148 -0
- package/dist/physics.js +35 -0
- package/dist/physics.js.map +1 -0
- package/dist/ports.d.ts +15 -0
- package/dist/ports.js.map +1 -1
- package/dist/runtime.d.ts +93 -4
- package/dist/runtime.js +299 -37
- package/dist/runtime.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"near-pointer-contract-cases.js","sourceRoot":"","sources":["../src/near-pointer-contract-cases.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared near-pointer conformance suite: the touch press, the grip grab,\n * pointer priority with its selection lock, and the ray and cursor visuals\n * (`near-pointer.ts`), run through a binding's own `HitTester` so every\n * platform proves the same rule over its own scene queries.\n *\n * Runner-free, like `hitTesterContractCases()`: an adapter's test file is a\n * loop over these cases with the same subject factory it already uses for\n * the hit-tester suite. The suite drives an `InteractionRuntime` itself, with\n * a scripted provider, so the only platform code under test is the hit\n * tester and the targets the driver places.\n *\n * Every distance below is metres to the target's SURFACE, the quantity the\n * touch and grab pointers compare (IWSDK's sphere intersector reports the\n * distance from the fingertip to the point on the mesh).\n */\nimport {\n NO_CAPABILITIES,\n type InputCapabilities,\n type InputProvider,\n type InputSourceSnapshot,\n type Vec3Tuple,\n} from \"@realitycollective/webxr-input\";\nimport type { HitTesterContractSubject } from \"./contract-cases.js\";\nimport type { InteractionEvent } from \"./events.js\";\nimport { NEAR_POINTER_DEFAULTS, type PointerVisuals } from \"./near-pointer.js\";\nimport { InteractionRuntime } from \"./runtime.js\";\n\n/** One check a binding's hit tester must pass under the near-pointer rule. */\nexport interface NearPointerContractCase {\n name: string;\n run(subject: HitTesterContractSubject): void;\n}\n\n/**\n * The shared near-pointer conformance suite. An adapter's test file:\n *\n * ```ts\n * for (const contractCase of nearPointerContractCases()) {\n * it(contractCase.name, () => contractCase.run(makeSubject()));\n * }\n * ```\n */\nexport function nearPointerContractCases(): readonly NearPointerContractCase[] {\n return NEAR_POINTER_CASES;\n}\n\n/** A provider whose sources a case sets frame by frame. */\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: false,\n gaze: false,\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\nconst CENTRE: Vec3Tuple = [0, 1, -1];\nconst RADIUS = 0.1;\n/** A ray from the viewer straight through the target. */\nconst RAY_AT = { origin: [0, 1, 0] as Vec3Tuple, direction: [0, 0, -1] as Vec3Tuple };\n/** A ray pointing away from everything. */\nconst RAY_AWAY = { origin: [0, 1, 0] as Vec3Tuple, direction: [0, 0, 1] as Vec3Tuple };\nconst FAR: Vec3Tuple = [0, 1, 1];\n\n/** A point `distance` metres outside the target's surface, on its near side. */\nfunction outside(distance: number): Vec3Tuple {\n return [CENTRE[0], CENTRE[1], CENTRE[2] + RADIUS + distance];\n}\n\ninterface HandFrame {\n tip?: Vec3Tuple;\n grip?: Vec3Tuple;\n ray?: \"at\" | \"away\" | \"none\";\n select?: number;\n squeeze?: number;\n kind?: \"hand\" | \"controller\";\n}\n\nfunction hand(frame: HandFrame): InputSourceSnapshot {\n const snapshot: InputSourceSnapshot = {\n id: \"right\",\n kind: frame.kind ?? \"hand\",\n handedness: \"right\",\n select: frame.select ?? 0,\n squeeze: frame.squeeze ?? 0,\n };\n const ray = frame.ray ?? \"away\";\n if (ray !== \"none\") snapshot.ray = ray === \"at\" ? { ...RAY_AT } : { ...RAY_AWAY };\n snapshot.indexTip = frame.tip ?? FAR;\n snapshot.gripPose = { position: frame.grip ?? FAR, quaternion: [0, 0, 0, 1] };\n return snapshot;\n}\n\ninterface Rig {\n runtime: InteractionRuntime;\n provider: ScriptedProvider;\n events: InteractionEvent[];\n visuals: PointerVisuals[];\n /** Feed one frame and return the event types it produced. */\n frame(frame: HandFrame): string[];\n}\n\nfunction rig(subject: HitTesterContractSubject, behaviours: Array<{ kind: \"press\" } | { kind: \"grab\" }> = [{ kind: \"press\" }]): Rig {\n subject.driver.place(\"target\", CENTRE, RADIUS);\n const provider = new ScriptedProvider();\n const runtime = new InteractionRuntime({ provider, hitTester: subject.hitTester, velocity: false });\n const events: InteractionEvent[] = [];\n const visuals: PointerVisuals[] = [];\n runtime.onEvent((event) => events.push(event));\n runtime.onPointerVisuals((list) => {\n visuals.length = 0;\n visuals.push(...list);\n });\n runtime.registerInteractable({ id: \"target\", behaviours });\n return {\n runtime,\n provider,\n events,\n visuals,\n frame(frame) {\n const before = events.length;\n provider.sources = [hand(frame)];\n runtime.update(1 / 60);\n return events.slice(before).map((event) => event.type);\n },\n };\n}\n\nfunction assert(condition: boolean, message: string): asserts condition {\n if (!condition) throw new Error(message);\n}\n\nfunction expectActive(r: Rig, kind: string | null, when: string): void {\n const active = r.runtime.getActivePointer(\"right\");\n assert(active === kind, `${when}: the active pointer must be ${String(kind)}, got ${String(active)}`);\n}\n\nconst D = NEAR_POINTER_DEFAULTS;\n\nconst NEAR_POINTER_CASES: readonly NearPointerContractCase[] = [\n {\n 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\",\n run(subject) {\n const r = rig(subject);\n r.frame({ tip: outside(0.3) });\n expectActive(r, null, \"at 0.3 m\");\n let types = r.frame({ tip: outside(D.touchHoverEnter - 0.01) });\n expectActive(r, \"touch\", \"inside the hover enter distance\");\n assert(types.includes(\"hoverEnter\"), \"entering the hover band must raise hoverEnter\");\n types = r.frame({ tip: outside(0.05) });\n assert(!types.includes(\"pressStart\"), \"5 cm out must not press\");\n types = r.frame({ tip: outside(D.touchDown) });\n assert(types.includes(\"pressStart\"), \"reaching the press distance must raise pressStart\");\n assert(r.runtime.getState(\"target\")?.pressed === true, \"the target must be pressed\");\n types = r.frame({ tip: outside(0.005) });\n assert(!types.includes(\"pressEnd\") && !types.includes(\"pressStart\"), \"going deeper must change nothing\");\n types = r.frame({ tip: outside(D.touchDown + 0.01) });\n assert(types.includes(\"pressEnd\"), \"coming back out past the press distance must release\");\n r.frame({ tip: outside(D.touchHoverEnter + 0.02) });\n expectActive(r, \"touch\", \"between enter and exit while hovering (hysteresis)\");\n types = r.frame({ tip: outside(D.touchHoverExit + 0.01) });\n expectActive(r, null, \"beyond the exit distance\");\n assert(types.includes(\"hoverExit\"), \"leaving the exit distance must raise hoverExit\");\n },\n },\n {\n name: \"a fingertip first seen inside the press band never presses until it has been outside it\",\n run(subject) {\n const r = rig(subject);\n let types = r.frame({ tip: outside(0.01) });\n assert(types.includes(\"hoverEnter\") && !types.includes(\"pressStart\"), \"a fingertip that appears inside the band hovers but must not press\");\n types = r.frame({ tip: outside(0.0) });\n assert(!types.includes(\"pressStart\"), \"staying inside must not press\");\n r.frame({ tip: outside(0.05) });\n types = r.frame({ tip: outside(0.015) });\n assert(types.includes(\"pressStart\"), \"coming back in from outside must press\");\n },\n },\n {\n name: \"a grip within 0.07 m of the surface makes grab the active pointer and a squeeze grabs that target\",\n run(subject) {\n const r = rig(subject, [{ kind: \"grab\" }]);\n r.frame({ grip: outside(D.grabRadius - 0.02), ray: \"at\" });\n expectActive(r, \"grab\", \"grip inside the grab radius, ray also hitting\");\n let types = r.frame({ grip: outside(D.grabRadius - 0.02), ray: \"at\", squeeze: 1 });\n assert(types.includes(\"grabStart\"), \"a squeeze while grab owns the hand must grab\");\n types = r.frame({ grip: outside(D.grabRadius - 0.02), ray: \"at\", squeeze: 0 });\n assert(types.includes(\"grabEnd\"), \"releasing the squeeze must end the grab\");\n r.frame({ grip: outside(D.grabRadius + 0.01), ray: \"away\" });\n expectActive(r, null, \"grip outside the grab radius with no ray hit\");\n },\n },\n {\n name: \"touch beats grab beats ray\",\n run(subject) {\n const r = rig(subject);\n r.frame({ tip: outside(0.1), grip: outside(0.05), ray: \"at\" });\n expectActive(r, \"touch\", \"fingertip, grip and ray all on the target\");\n r.frame({ grip: outside(0.05), ray: \"at\" });\n expectActive(r, \"grab\", \"grip and ray on the target\");\n r.frame({ ray: \"at\" });\n expectActive(r, \"ray\", \"only the ray on the target\");\n r.frame({ ray: \"away\" });\n expectActive(r, null, \"nothing on the target\");\n },\n },\n {\n name: \"the ray and its cursor show only while the ray owns the source, and the cursor sits at the active pointer's hit\",\n run(subject) {\n const r = rig(subject);\n r.frame({ ray: \"at\" });\n let v = r.visuals[0]!;\n assert(v.ray && v.cursor && v.cursorPoint !== null, \"with the ray active, the ray and its cursor show\");\n assert(Math.abs(v.cursorPoint![2] - (CENTRE[2] + RADIUS)) <= RADIUS + 0.05, \"the cursor sits near the ray's hit on the target\");\n r.frame({ tip: outside(0.05), ray: \"at\" });\n v = r.visuals[0]!;\n assert(!v.ray, \"with touch active, the ray is hidden\");\n assert(v.cursor && v.cursorPoint !== null, \"with touch active, the cursor shows at the touch point\");\n assert(Math.abs(v.cursorPoint![2] - (CENTRE[2] + RADIUS)) <= RADIUS + 0.05, \"the cursor sits on the surface under the fingertip\");\n r.frame({ grip: outside(0.05), ray: \"at\" });\n v = r.visuals[0]!;\n assert(!v.ray && v.cursor, \"with grab active, the ray is hidden and the cursor shows\");\n r.frame({ ray: \"away\" });\n v = r.visuals[0]!;\n assert(v.ray && !v.cursor && v.cursorPoint === null, \"with nothing active, the ray shows and the cursor does not\");\n r.frame({ ray: \"none\" });\n v = r.visuals[0]!;\n assert(!v.ray && !v.cursor, \"a source with no ray draws no ray\");\n },\n },\n {\n name: \"a pointer that is pressing keeps the source until it releases\",\n run(subject) {\n const r = rig(subject);\n r.frame({ ray: \"at\" });\n let types = r.frame({ ray: \"at\", select: 1 });\n assert(types.includes(\"pressStart\"), \"a select on the ray target presses it\");\n r.frame({ tip: outside(0.05), ray: \"at\", select: 1 });\n expectActive(r, \"ray\", \"a fingertip entering the band while the ray presses\");\n types = r.frame({ tip: outside(0.05), ray: \"at\", select: 0 });\n assert(types.includes(\"pressEnd\"), \"releasing select ends the ray press\");\n r.frame({ tip: outside(0.05), ray: \"at\" });\n expectActive(r, \"touch\", \"once released, touch takes the source\");\n },\n },\n {\n name: \"a controller's ray origin presses by touch exactly as a fingertip does\",\n run(subject) {\n const r = rig(subject);\n r.frame({ kind: \"controller\", tip: outside(0.1) });\n expectActive(r, \"touch\", \"a controller nose inside the hover band\");\n const types = r.frame({ kind: \"controller\", tip: outside(0.01) });\n assert(types.includes(\"pressStart\"), \"a controller nose reaching the press distance presses\");\n },\n },\n];\n"]}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Near pointers - the touch press, the grip grab, which pointer owns a hand
|
|
3
|
+
* and what that means for the ray and cursor. Pure logic, no engine.
|
|
4
|
+
*
|
|
5
|
+
* The template is IWSDK 1.0.0's pointer stack (`@iwsdk/xr-input`
|
|
6
|
+
* `multi-pointer.js`, `touch-pointer.js`, `grab-pointer.js`, over
|
|
7
|
+
* `@pmndrs/pointer-events`), restated here so every binding runs the same
|
|
8
|
+
* rule from the facts it already reports (`indexTip`, `gripPose`, `ray`):
|
|
9
|
+
*
|
|
10
|
+
* - **Touch.** A fingertip (a controller's ray origin stands in for one)
|
|
11
|
+
* hovers a target once its distance to the target's SURFACE is within
|
|
12
|
+
* `touchHoverEnter` (0.15 m) and keeps hovering until it is beyond
|
|
13
|
+
* `touchHoverExit` (0.2 m): `TOUCH_HYSTERESIS`. It presses while that
|
|
14
|
+
* distance is at or under `touchDown` (0.02 m): `TOUCH_DEFAULTS.downRadius`,
|
|
15
|
+
* which `@pmndrs/pointer-events` compares every frame, so the release is
|
|
16
|
+
* the way out past the same distance.
|
|
17
|
+
* - **Grab.** The grip is a sphere of `grabRadius` (0.07 m,
|
|
18
|
+
* `createGrabPointer`'s default) and a target whose surface it reaches is
|
|
19
|
+
* the grab candidate. The squeeze (or a hand's pinch on a grab-only
|
|
20
|
+
* target) then grabs it.
|
|
21
|
+
* - **Priority.** Per source, the first of touch, grab, ray with a candidate
|
|
22
|
+
* is the active pointer (`PRIORITY_ORDER`), and once it is pressing or
|
|
23
|
+
* grabbing it stays active until the release (IWSDK's selection lock).
|
|
24
|
+
* - **Visuals.** The ray and its cursor show only while the ray is the active
|
|
25
|
+
* pointer or nothing is (`shouldHideRay`); a near pointer owning the hand
|
|
26
|
+
* hides them, and the shared cursor sits at the active pointer's hit point.
|
|
27
|
+
*
|
|
28
|
+
* One rule is deliberately stricter than the template, as the Pale Signal
|
|
29
|
+
* work order of 28 September 2026 required: a touch never presses from inside
|
|
30
|
+
* or from behind. A fingertip first seen already within the press band, a
|
|
31
|
+
* hand that pops into tracking inside the target, does not press until it has
|
|
32
|
+
* been seen outside the band and comes back in. IWSDK's touch pointer would
|
|
33
|
+
* press on that first frame; `touch-reference.test.ts` records the one frame
|
|
34
|
+
* on which the two differ, and that they agree everywhere else.
|
|
35
|
+
*/
|
|
36
|
+
/** The pointer that owns a source this frame. `"gaze"` is the eye-gaze source's own pointer. */
|
|
37
|
+
export type ActivePointerKind = "touch" | "grab" | "ray" | "gaze";
|
|
38
|
+
/** IWSDK's order: the first with a candidate wins. `multi-pointer.js` `PRIORITY_ORDER`. */
|
|
39
|
+
export declare const POINTER_PRIORITY: readonly ActivePointerKind[];
|
|
40
|
+
export interface NearPointerOptions {
|
|
41
|
+
/** Surface distance, metres, within which a fingertip starts hovering a target. IWSDK `enterHoverDistance` 0.15. */
|
|
42
|
+
touchHoverEnter: number;
|
|
43
|
+
/** Surface distance, metres, beyond which a hovering fingertip stops hovering. IWSDK `exitHoverDistance` 0.2. Never less than `touchHoverEnter`. */
|
|
44
|
+
touchHoverExit: number;
|
|
45
|
+
/** Surface distance, metres, at or under which a hovering fingertip presses, and above which it releases. IWSDK `downRadius` 0.02. */
|
|
46
|
+
touchDown: number;
|
|
47
|
+
/** Radius, metres, of the grip sphere that finds a grab candidate. `createGrabPointer` default 0.07. */
|
|
48
|
+
grabRadius: number;
|
|
49
|
+
}
|
|
50
|
+
/** IWSDK 1.0.0's constants: `TOUCH_HYSTERESIS`, `TOUCH_DEFAULTS.downRadius`, `createGrabPointer`'s radius. */
|
|
51
|
+
export declare const NEAR_POINTER_DEFAULTS: Readonly<NearPointerOptions>;
|
|
52
|
+
export declare function resolveNearPointerOptions(options?: Partial<NearPointerOptions>): NearPointerOptions;
|
|
53
|
+
/** What the touch machine reports after one frame. */
|
|
54
|
+
export interface TouchPointerUpdate {
|
|
55
|
+
/** The fingertip is hovering a target this frame (the touch pointer has a candidate). */
|
|
56
|
+
hovering: boolean;
|
|
57
|
+
/** The touch is pressing this frame. */
|
|
58
|
+
pressed: boolean;
|
|
59
|
+
/** True on the exact frame a press started. */
|
|
60
|
+
pressStarted: boolean;
|
|
61
|
+
/** True on the exact frame a press ended. */
|
|
62
|
+
pressEnded: boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* One source's touch pointer: hover with hysteresis, press at `touchDown`
|
|
66
|
+
* armed only from outside the band. Feed it the fingertip's surface distance
|
|
67
|
+
* to the nearest target each frame, or `null` when no target is in reach.
|
|
68
|
+
*/
|
|
69
|
+
export declare class TouchPointerState {
|
|
70
|
+
private hovering;
|
|
71
|
+
private pressed;
|
|
72
|
+
/** The fingertip has been seen outside the press band since it last lost contact. */
|
|
73
|
+
private armed;
|
|
74
|
+
get isHovering(): boolean;
|
|
75
|
+
get isPressed(): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Advance one frame. `enter`/`exit` are the hover band for the target in
|
|
78
|
+
* question (an interactable may widen the default), `down` the press
|
|
79
|
+
* distance.
|
|
80
|
+
*/
|
|
81
|
+
update(distance: number | null, enter: number, exit: number, down: number): TouchPointerUpdate;
|
|
82
|
+
/** Drop everything without reporting (the source went away). */
|
|
83
|
+
reset(): void;
|
|
84
|
+
}
|
|
85
|
+
/** A candidate one pointer found: the target and the point the cursor sits at. */
|
|
86
|
+
export interface PointerCandidate {
|
|
87
|
+
targetId: string;
|
|
88
|
+
/** World-space point: the ray's hit, or the closest point of a near target. */
|
|
89
|
+
point: readonly [number, number, number];
|
|
90
|
+
/** Metres: the ray parameter for a ray, the surface distance for a near pointer. */
|
|
91
|
+
distance: number;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* IWSDK's `pickActiveByPriority` with its selection lock: while the current
|
|
95
|
+
* active pointer is selecting it stays; otherwise the first pointer in
|
|
96
|
+
* `POINTER_PRIORITY` with a candidate wins, or none.
|
|
97
|
+
*/
|
|
98
|
+
export declare function pickActivePointer(candidates: {
|
|
99
|
+
touch: boolean;
|
|
100
|
+
grab: boolean;
|
|
101
|
+
ray: boolean;
|
|
102
|
+
}, current: ActivePointerKind | null, selecting: boolean): ActivePointerKind | null;
|
|
103
|
+
/**
|
|
104
|
+
* What a binding shows for one source: IWSDK's `shouldHideRay` and the shared
|
|
105
|
+
* cursor from `MultiPointer.update`. A host draws exactly this and decides
|
|
106
|
+
* nothing.
|
|
107
|
+
*/
|
|
108
|
+
export interface PointerVisuals {
|
|
109
|
+
/** The source these visuals belong to. */
|
|
110
|
+
sourceId: string;
|
|
111
|
+
/** The pointer owning the source, or null when none has a candidate. */
|
|
112
|
+
activePointer: ActivePointerKind | null;
|
|
113
|
+
/**
|
|
114
|
+
* Draw this source's ray. True while the source has a ray and no near
|
|
115
|
+
* pointer owns it: the ray is active, or nothing is. False while touch or
|
|
116
|
+
* grab owns the hand, while eye gaze has taken the far ray, and for a
|
|
117
|
+
* source with no ray at all.
|
|
118
|
+
*/
|
|
119
|
+
ray: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Draw the cursor disc at `cursorPoint`. True exactly while the active
|
|
122
|
+
* pointer has a candidate: at the ray's hit, or on the surface the
|
|
123
|
+
* fingertip or grip reaches. Never drawn "at every ray hit" regardless of
|
|
124
|
+
* ownership, which is what the September 2026 native contract said.
|
|
125
|
+
*/
|
|
126
|
+
cursor: boolean;
|
|
127
|
+
/** World-space cursor position while `cursor` is true, else null. */
|
|
128
|
+
cursorPoint: readonly [number, number, number] | null;
|
|
129
|
+
}
|
|
130
|
+
/** Build the visuals for one source from its active pointer and that pointer's candidate. */
|
|
131
|
+
export declare function pointerVisualsFor(sourceId: string, hasRay: boolean, active: ActivePointerKind | null, candidate: PointerCandidate | null): PointerVisuals;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Near pointers - the touch press, the grip grab, which pointer owns a hand
|
|
3
|
+
* and what that means for the ray and cursor. Pure logic, no engine.
|
|
4
|
+
*
|
|
5
|
+
* The template is IWSDK 1.0.0's pointer stack (`@iwsdk/xr-input`
|
|
6
|
+
* `multi-pointer.js`, `touch-pointer.js`, `grab-pointer.js`, over
|
|
7
|
+
* `@pmndrs/pointer-events`), restated here so every binding runs the same
|
|
8
|
+
* rule from the facts it already reports (`indexTip`, `gripPose`, `ray`):
|
|
9
|
+
*
|
|
10
|
+
* - **Touch.** A fingertip (a controller's ray origin stands in for one)
|
|
11
|
+
* hovers a target once its distance to the target's SURFACE is within
|
|
12
|
+
* `touchHoverEnter` (0.15 m) and keeps hovering until it is beyond
|
|
13
|
+
* `touchHoverExit` (0.2 m): `TOUCH_HYSTERESIS`. It presses while that
|
|
14
|
+
* distance is at or under `touchDown` (0.02 m): `TOUCH_DEFAULTS.downRadius`,
|
|
15
|
+
* which `@pmndrs/pointer-events` compares every frame, so the release is
|
|
16
|
+
* the way out past the same distance.
|
|
17
|
+
* - **Grab.** The grip is a sphere of `grabRadius` (0.07 m,
|
|
18
|
+
* `createGrabPointer`'s default) and a target whose surface it reaches is
|
|
19
|
+
* the grab candidate. The squeeze (or a hand's pinch on a grab-only
|
|
20
|
+
* target) then grabs it.
|
|
21
|
+
* - **Priority.** Per source, the first of touch, grab, ray with a candidate
|
|
22
|
+
* is the active pointer (`PRIORITY_ORDER`), and once it is pressing or
|
|
23
|
+
* grabbing it stays active until the release (IWSDK's selection lock).
|
|
24
|
+
* - **Visuals.** The ray and its cursor show only while the ray is the active
|
|
25
|
+
* pointer or nothing is (`shouldHideRay`); a near pointer owning the hand
|
|
26
|
+
* hides them, and the shared cursor sits at the active pointer's hit point.
|
|
27
|
+
*
|
|
28
|
+
* One rule is deliberately stricter than the template, as the Pale Signal
|
|
29
|
+
* work order of 28 September 2026 required: a touch never presses from inside
|
|
30
|
+
* or from behind. A fingertip first seen already within the press band, a
|
|
31
|
+
* hand that pops into tracking inside the target, does not press until it has
|
|
32
|
+
* been seen outside the band and comes back in. IWSDK's touch pointer would
|
|
33
|
+
* press on that first frame; `touch-reference.test.ts` records the one frame
|
|
34
|
+
* on which the two differ, and that they agree everywhere else.
|
|
35
|
+
*/
|
|
36
|
+
/** IWSDK's order: the first with a candidate wins. `multi-pointer.js` `PRIORITY_ORDER`. */
|
|
37
|
+
export const POINTER_PRIORITY = Object.freeze(["touch", "grab", "ray"]);
|
|
38
|
+
/** IWSDK 1.0.0's constants: `TOUCH_HYSTERESIS`, `TOUCH_DEFAULTS.downRadius`, `createGrabPointer`'s radius. */
|
|
39
|
+
export const NEAR_POINTER_DEFAULTS = Object.freeze({
|
|
40
|
+
touchHoverEnter: 0.15,
|
|
41
|
+
touchHoverExit: 0.2,
|
|
42
|
+
touchDown: 0.02,
|
|
43
|
+
grabRadius: 0.07,
|
|
44
|
+
});
|
|
45
|
+
export function resolveNearPointerOptions(options = {}) {
|
|
46
|
+
const resolved = { ...NEAR_POINTER_DEFAULTS, ...options };
|
|
47
|
+
if (resolved.touchDown < 0 || resolved.touchHoverEnter < 0 || resolved.grabRadius < 0) {
|
|
48
|
+
throw new Error("[webxr-interactions] near pointer distances must be >= 0");
|
|
49
|
+
}
|
|
50
|
+
if (resolved.touchHoverExit < resolved.touchHoverEnter) {
|
|
51
|
+
throw new Error(`[webxr-interactions] touchHoverExit (${resolved.touchHoverExit}) must be >= touchHoverEnter (${resolved.touchHoverEnter})`);
|
|
52
|
+
}
|
|
53
|
+
return resolved;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One source's touch pointer: hover with hysteresis, press at `touchDown`
|
|
57
|
+
* armed only from outside the band. Feed it the fingertip's surface distance
|
|
58
|
+
* to the nearest target each frame, or `null` when no target is in reach.
|
|
59
|
+
*/
|
|
60
|
+
export class TouchPointerState {
|
|
61
|
+
hovering = false;
|
|
62
|
+
pressed = false;
|
|
63
|
+
/** The fingertip has been seen outside the press band since it last lost contact. */
|
|
64
|
+
armed = false;
|
|
65
|
+
get isHovering() {
|
|
66
|
+
return this.hovering;
|
|
67
|
+
}
|
|
68
|
+
get isPressed() {
|
|
69
|
+
return this.pressed;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Advance one frame. `enter`/`exit` are the hover band for the target in
|
|
73
|
+
* question (an interactable may widen the default), `down` the press
|
|
74
|
+
* distance.
|
|
75
|
+
*/
|
|
76
|
+
update(distance, enter, exit, down) {
|
|
77
|
+
const wasPressed = this.pressed;
|
|
78
|
+
const threshold = this.hovering ? exit : enter;
|
|
79
|
+
if (distance === null || distance > threshold) {
|
|
80
|
+
this.hovering = false;
|
|
81
|
+
this.pressed = false;
|
|
82
|
+
this.armed = false;
|
|
83
|
+
return { hovering: false, pressed: false, pressStarted: false, pressEnded: wasPressed };
|
|
84
|
+
}
|
|
85
|
+
this.hovering = true;
|
|
86
|
+
if (distance > down) {
|
|
87
|
+
// Outside the press band: released if it was pressing, and armed for the next press.
|
|
88
|
+
this.armed = true;
|
|
89
|
+
this.pressed = false;
|
|
90
|
+
return { hovering: true, pressed: false, pressStarted: false, pressEnded: wasPressed };
|
|
91
|
+
}
|
|
92
|
+
// Within the press band. IWSDK presses here unconditionally; the core
|
|
93
|
+
// requires the fingertip to have arrived from outside (see the file comment).
|
|
94
|
+
if (!this.pressed && this.armed) {
|
|
95
|
+
this.pressed = true;
|
|
96
|
+
this.armed = false;
|
|
97
|
+
return { hovering: true, pressed: true, pressStarted: true, pressEnded: false };
|
|
98
|
+
}
|
|
99
|
+
return { hovering: true, pressed: this.pressed, pressStarted: false, pressEnded: false };
|
|
100
|
+
}
|
|
101
|
+
/** Drop everything without reporting (the source went away). */
|
|
102
|
+
reset() {
|
|
103
|
+
this.hovering = false;
|
|
104
|
+
this.pressed = false;
|
|
105
|
+
this.armed = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* IWSDK's `pickActiveByPriority` with its selection lock: while the current
|
|
110
|
+
* active pointer is selecting it stays; otherwise the first pointer in
|
|
111
|
+
* `POINTER_PRIORITY` with a candidate wins, or none.
|
|
112
|
+
*/
|
|
113
|
+
export function pickActivePointer(candidates, current, selecting) {
|
|
114
|
+
if (selecting && current !== null)
|
|
115
|
+
return current;
|
|
116
|
+
for (const kind of POINTER_PRIORITY) {
|
|
117
|
+
if (candidates[kind])
|
|
118
|
+
return kind;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/** Build the visuals for one source from its active pointer and that pointer's candidate. */
|
|
123
|
+
export function pointerVisualsFor(sourceId, hasRay, active, candidate) {
|
|
124
|
+
const ray = hasRay && (active === null || active === "ray");
|
|
125
|
+
const cursor = active !== null && candidate !== null;
|
|
126
|
+
return { sourceId, activePointer: active, ray, cursor, cursorPoint: cursor ? candidate.point : null };
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=near-pointer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"near-pointer.js","sourceRoot":"","sources":["../src/near-pointer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Near pointers - the touch press, the grip grab, which pointer owns a hand\n * and what that means for the ray and cursor. Pure logic, no engine.\n *\n * The template is IWSDK 1.0.0's pointer stack (`@iwsdk/xr-input`\n * `multi-pointer.js`, `touch-pointer.js`, `grab-pointer.js`, over\n * `@pmndrs/pointer-events`), restated here so every binding runs the same\n * rule from the facts it already reports (`indexTip`, `gripPose`, `ray`):\n *\n * - **Touch.** A fingertip (a controller's ray origin stands in for one)\n * hovers a target once its distance to the target's SURFACE is within\n * `touchHoverEnter` (0.15 m) and keeps hovering until it is beyond\n * `touchHoverExit` (0.2 m): `TOUCH_HYSTERESIS`. It presses while that\n * distance is at or under `touchDown` (0.02 m): `TOUCH_DEFAULTS.downRadius`,\n * which `@pmndrs/pointer-events` compares every frame, so the release is\n * the way out past the same distance.\n * - **Grab.** The grip is a sphere of `grabRadius` (0.07 m,\n * `createGrabPointer`'s default) and a target whose surface it reaches is\n * the grab candidate. The squeeze (or a hand's pinch on a grab-only\n * target) then grabs it.\n * - **Priority.** Per source, the first of touch, grab, ray with a candidate\n * is the active pointer (`PRIORITY_ORDER`), and once it is pressing or\n * grabbing it stays active until the release (IWSDK's selection lock).\n * - **Visuals.** The ray and its cursor show only while the ray is the active\n * pointer or nothing is (`shouldHideRay`); a near pointer owning the hand\n * hides them, and the shared cursor sits at the active pointer's hit point.\n *\n * One rule is deliberately stricter than the template, as the Pale Signal\n * work order of 28 September 2026 required: a touch never presses from inside\n * or from behind. A fingertip first seen already within the press band, a\n * hand that pops into tracking inside the target, does not press until it has\n * been seen outside the band and comes back in. IWSDK's touch pointer would\n * press on that first frame; `touch-reference.test.ts` records the one frame\n * on which the two differ, and that they agree everywhere else.\n */\n\n/** The pointer that owns a source this frame. `\"gaze\"` is the eye-gaze source's own pointer. */\nexport type ActivePointerKind = \"touch\" | \"grab\" | \"ray\" | \"gaze\";\n\n/** IWSDK's order: the first with a candidate wins. `multi-pointer.js` `PRIORITY_ORDER`. */\nexport const POINTER_PRIORITY: readonly ActivePointerKind[] = Object.freeze([\"touch\", \"grab\", \"ray\"]);\n\nexport interface NearPointerOptions {\n /** Surface distance, metres, within which a fingertip starts hovering a target. IWSDK `enterHoverDistance` 0.15. */\n touchHoverEnter: number;\n /** Surface distance, metres, beyond which a hovering fingertip stops hovering. IWSDK `exitHoverDistance` 0.2. Never less than `touchHoverEnter`. */\n touchHoverExit: number;\n /** Surface distance, metres, at or under which a hovering fingertip presses, and above which it releases. IWSDK `downRadius` 0.02. */\n touchDown: number;\n /** Radius, metres, of the grip sphere that finds a grab candidate. `createGrabPointer` default 0.07. */\n grabRadius: number;\n}\n\n/** IWSDK 1.0.0's constants: `TOUCH_HYSTERESIS`, `TOUCH_DEFAULTS.downRadius`, `createGrabPointer`'s radius. */\nexport const NEAR_POINTER_DEFAULTS: Readonly<NearPointerOptions> = Object.freeze({\n touchHoverEnter: 0.15,\n touchHoverExit: 0.2,\n touchDown: 0.02,\n grabRadius: 0.07,\n});\n\nexport function resolveNearPointerOptions(options: Partial<NearPointerOptions> = {}): NearPointerOptions {\n const resolved = { ...NEAR_POINTER_DEFAULTS, ...options };\n if (resolved.touchDown < 0 || resolved.touchHoverEnter < 0 || resolved.grabRadius < 0) {\n throw new Error(\"[webxr-interactions] near pointer distances must be >= 0\");\n }\n if (resolved.touchHoverExit < resolved.touchHoverEnter) {\n throw new Error(\n `[webxr-interactions] touchHoverExit (${resolved.touchHoverExit}) must be >= touchHoverEnter (${resolved.touchHoverEnter})`,\n );\n }\n return resolved;\n}\n\n/** What the touch machine reports after one frame. */\nexport interface TouchPointerUpdate {\n /** The fingertip is hovering a target this frame (the touch pointer has a candidate). */\n hovering: boolean;\n /** The touch is pressing this frame. */\n pressed: boolean;\n /** True on the exact frame a press started. */\n pressStarted: boolean;\n /** True on the exact frame a press ended. */\n pressEnded: boolean;\n}\n\n/**\n * One source's touch pointer: hover with hysteresis, press at `touchDown`\n * armed only from outside the band. Feed it the fingertip's surface distance\n * to the nearest target each frame, or `null` when no target is in reach.\n */\nexport class TouchPointerState {\n private hovering = false;\n private pressed = false;\n /** The fingertip has been seen outside the press band since it last lost contact. */\n private armed = false;\n\n get isHovering(): boolean {\n return this.hovering;\n }\n\n get isPressed(): boolean {\n return this.pressed;\n }\n\n /**\n * Advance one frame. `enter`/`exit` are the hover band for the target in\n * question (an interactable may widen the default), `down` the press\n * distance.\n */\n update(distance: number | null, enter: number, exit: number, down: number): TouchPointerUpdate {\n const wasPressed = this.pressed;\n const threshold = this.hovering ? exit : enter;\n if (distance === null || distance > threshold) {\n this.hovering = false;\n this.pressed = false;\n this.armed = false;\n return { hovering: false, pressed: false, pressStarted: false, pressEnded: wasPressed };\n }\n this.hovering = true;\n if (distance > down) {\n // Outside the press band: released if it was pressing, and armed for the next press.\n this.armed = true;\n this.pressed = false;\n return { hovering: true, pressed: false, pressStarted: false, pressEnded: wasPressed };\n }\n // Within the press band. IWSDK presses here unconditionally; the core\n // requires the fingertip to have arrived from outside (see the file comment).\n if (!this.pressed && this.armed) {\n this.pressed = true;\n this.armed = false;\n return { hovering: true, pressed: true, pressStarted: true, pressEnded: false };\n }\n return { hovering: true, pressed: this.pressed, pressStarted: false, pressEnded: false };\n }\n\n /** Drop everything without reporting (the source went away). */\n reset(): void {\n this.hovering = false;\n this.pressed = false;\n this.armed = false;\n }\n}\n\n/** A candidate one pointer found: the target and the point the cursor sits at. */\nexport interface PointerCandidate {\n targetId: string;\n /** World-space point: the ray's hit, or the closest point of a near target. */\n point: readonly [number, number, number];\n /** Metres: the ray parameter for a ray, the surface distance for a near pointer. */\n distance: number;\n}\n\n/**\n * IWSDK's `pickActiveByPriority` with its selection lock: while the current\n * active pointer is selecting it stays; otherwise the first pointer in\n * `POINTER_PRIORITY` with a candidate wins, or none.\n */\nexport function pickActivePointer(\n candidates: { touch: boolean; grab: boolean; ray: boolean },\n current: ActivePointerKind | null,\n selecting: boolean,\n): ActivePointerKind | null {\n if (selecting && current !== null) return current;\n for (const kind of POINTER_PRIORITY) {\n if (candidates[kind as \"touch\" | \"grab\" | \"ray\"]) return kind;\n }\n return null;\n}\n\n/**\n * What a binding shows for one source: IWSDK's `shouldHideRay` and the shared\n * cursor from `MultiPointer.update`. A host draws exactly this and decides\n * nothing.\n */\nexport interface PointerVisuals {\n /** The source these visuals belong to. */\n sourceId: string;\n /** The pointer owning the source, or null when none has a candidate. */\n activePointer: ActivePointerKind | null;\n /**\n * Draw this source's ray. True while the source has a ray and no near\n * pointer owns it: the ray is active, or nothing is. False while touch or\n * grab owns the hand, while eye gaze has taken the far ray, and for a\n * source with no ray at all.\n */\n ray: boolean;\n /**\n * Draw the cursor disc at `cursorPoint`. True exactly while the active\n * pointer has a candidate: at the ray's hit, or on the surface the\n * fingertip or grip reaches. Never drawn \"at every ray hit\" regardless of\n * ownership, which is what the September 2026 native contract said.\n */\n cursor: boolean;\n /** World-space cursor position while `cursor` is true, else null. */\n cursorPoint: readonly [number, number, number] | null;\n}\n\n/** Build the visuals for one source from its active pointer and that pointer's candidate. */\nexport function pointerVisualsFor(\n sourceId: string,\n hasRay: boolean,\n active: ActivePointerKind | null,\n candidate: PointerCandidate | null,\n): PointerVisuals {\n const ray = hasRay && (active === null || active === \"ray\");\n const cursor = active !== null && candidate !== null;\n return { sourceId, activePointer: active, ray, cursor, cursorPoint: cursor ? candidate!.point : null };\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type PhysicsFacility } from "./physics.js";
|
|
2
|
+
/** The facility under test. Build a FRESH one per case. */
|
|
3
|
+
export interface PhysicsFacilityContractSubject {
|
|
4
|
+
facility: PhysicsFacility;
|
|
5
|
+
}
|
|
6
|
+
/** One check a {@link PhysicsFacility} implementation must pass. */
|
|
7
|
+
export interface PhysicsFacilityContractCase {
|
|
8
|
+
name: string;
|
|
9
|
+
run(subject: PhysicsFacilityContractSubject): void;
|
|
10
|
+
}
|
|
11
|
+
export declare function physicsFacilityContractCases(): readonly PhysicsFacilityContractCase[];
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { PHYSICS_DEFAULTS } from "./physics.js";
|
|
2
|
+
export function physicsFacilityContractCases() {
|
|
3
|
+
return PHYSICS_CASES;
|
|
4
|
+
}
|
|
5
|
+
const DT = 1 / PHYSICS_DEFAULTS.stepHz;
|
|
6
|
+
const BALL_RADIUS = 0.1;
|
|
7
|
+
const DROP_HEIGHT = 1;
|
|
8
|
+
/** Steps in three seconds: enough for a 1 m drop to land and settle on every engine. */
|
|
9
|
+
const SETTLE_STEPS = 180;
|
|
10
|
+
const IDENTITY = [0, 0, 0, 1];
|
|
11
|
+
const REST_EPS = 0.02;
|
|
12
|
+
const REST_SPEED = 0.05;
|
|
13
|
+
const HOLD_EPS = 1e-3;
|
|
14
|
+
function pose(x, y, z) {
|
|
15
|
+
return { position: [x, y, z], quaternion: [...IDENTITY] };
|
|
16
|
+
}
|
|
17
|
+
/** A static box floor, 20 m square and 0.1 m thick, whose top face is y = 0. */
|
|
18
|
+
function floor(facility) {
|
|
19
|
+
facility.addBody("floor", pose(0, -0.05, 0), { state: "static" }, { kind: "box", dimensions: [20, 0.1, 20] });
|
|
20
|
+
}
|
|
21
|
+
function ball(facility, restitution = 0, gravityFactor = 1) {
|
|
22
|
+
facility.addBody("ball", pose(0, DROP_HEIGHT, 0), { gravityFactor }, { kind: "sphere", dimensions: [BALL_RADIUS, 0, 0], restitution });
|
|
23
|
+
}
|
|
24
|
+
function steps(facility, count) {
|
|
25
|
+
for (let i = 0; i < count; i++)
|
|
26
|
+
facility.step(DT);
|
|
27
|
+
}
|
|
28
|
+
function assert(condition, message) {
|
|
29
|
+
if (!condition)
|
|
30
|
+
throw new Error(message);
|
|
31
|
+
}
|
|
32
|
+
function close(a, b, eps) {
|
|
33
|
+
return Math.abs(a[0] - b[0]) <= eps && Math.abs(a[1] - b[1]) <= eps && Math.abs(a[2] - b[2]) <= eps;
|
|
34
|
+
}
|
|
35
|
+
function speed(v) {
|
|
36
|
+
return Math.hypot(v[0], v[1], v[2]);
|
|
37
|
+
}
|
|
38
|
+
const PHYSICS_CASES = [
|
|
39
|
+
{
|
|
40
|
+
name: "starts with IWSDK's gravity, and a body added with no spec is dynamic until removed",
|
|
41
|
+
run({ facility }) {
|
|
42
|
+
assert(close(facility.getGravity(), PHYSICS_DEFAULTS.gravity, 1e-6), `gravity must start at ${JSON.stringify(PHYSICS_DEFAULTS.gravity)}, got ${JSON.stringify(facility.getGravity())}`);
|
|
43
|
+
assert(typeof facility.engine === "string" && facility.engine.length > 0, "the facility must name its engine");
|
|
44
|
+
facility.addBody("ball", pose(0, 1, 0), undefined, { kind: "sphere", dimensions: [BALL_RADIUS, 0, 0] });
|
|
45
|
+
assert(facility.hasBody("ball"), "a body just added must be there");
|
|
46
|
+
assert(facility.getBodyState("ball") === "dynamic", `a body with no spec must be dynamic, got ${facility.getBodyState("ball")}`);
|
|
47
|
+
assert(facility.isSuspended("ball") === false, "a body just added is not suspended");
|
|
48
|
+
const at = facility.getBodyPose("ball");
|
|
49
|
+
assert(close(at.position, [0, 1, 0], 1e-6), `a body just added sits where it was added, got ${JSON.stringify(at.position)}`);
|
|
50
|
+
facility.removeBody("ball");
|
|
51
|
+
assert(!facility.hasBody("ball"), "a removed body must be gone");
|
|
52
|
+
let threw = false;
|
|
53
|
+
try {
|
|
54
|
+
facility.getBodyPose("ball");
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
threw = /no physics body/.test(error.message);
|
|
58
|
+
}
|
|
59
|
+
assert(threw, 'reading a missing body must throw an error naming it ("no physics body")');
|
|
60
|
+
facility.removeBody("ball");
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "a dynamic sphere falls under gravity and comes to rest on a static box",
|
|
65
|
+
run({ facility }) {
|
|
66
|
+
floor(facility);
|
|
67
|
+
ball(facility);
|
|
68
|
+
steps(facility, 30);
|
|
69
|
+
const falling = facility.getBodyPose("ball").position[1];
|
|
70
|
+
// Half a second of free fall drops about 1.2 m from rest; it has hit
|
|
71
|
+
// the floor by now, or is well on its way: at most 0.5 m of the drop left.
|
|
72
|
+
assert(falling < DROP_HEIGHT - 0.5, `after 0.5 s the ball must have fallen at least 0.5 m, it is at y = ${falling}`);
|
|
73
|
+
steps(facility, SETTLE_STEPS - 30);
|
|
74
|
+
const rest = facility.getBodyPose("ball").position;
|
|
75
|
+
assert(Math.abs(rest[1] - BALL_RADIUS) <= REST_EPS, `the ball must rest on the floor at y = ${BALL_RADIUS} (2 cm), it is at y = ${rest[1]}`);
|
|
76
|
+
assert(Math.abs(rest[0]) <= REST_EPS && Math.abs(rest[2]) <= REST_EPS, `the ball must not drift sideways, it is at ${JSON.stringify(rest)}`);
|
|
77
|
+
assert(speed(facility.getVelocity("ball").linear) <= REST_SPEED, `a resting ball must be still, its speed is ${speed(facility.getVelocity("ball").linear)}`);
|
|
78
|
+
const floorPose = facility.getBodyPose("floor");
|
|
79
|
+
assert(close(floorPose.position, [0, -0.05, 0], 1e-3), "a static floor never moves");
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "restitution bounces: a lively ball leaves the floor again, a dead one does not",
|
|
84
|
+
run({ facility }) {
|
|
85
|
+
floor(facility);
|
|
86
|
+
ball(facility, 0.8);
|
|
87
|
+
let landed = false;
|
|
88
|
+
let bounced = false;
|
|
89
|
+
for (let i = 0; i < SETTLE_STEPS && !bounced; i++) {
|
|
90
|
+
facility.step(DT);
|
|
91
|
+
const vy = facility.getVelocity("ball").linear[1];
|
|
92
|
+
if (vy < -0.5)
|
|
93
|
+
landed = true;
|
|
94
|
+
if (landed && vy > 0.5)
|
|
95
|
+
bounced = true;
|
|
96
|
+
}
|
|
97
|
+
assert(bounced, "a ball with restitution 0.8 dropped from 1 m must bounce back up at over 0.5 m/s");
|
|
98
|
+
facility.removeBody("ball");
|
|
99
|
+
ball(facility, 0);
|
|
100
|
+
landed = false;
|
|
101
|
+
let rose = 0;
|
|
102
|
+
for (let i = 0; i < SETTLE_STEPS; i++) {
|
|
103
|
+
facility.step(DT);
|
|
104
|
+
const vy = facility.getVelocity("ball").linear[1];
|
|
105
|
+
if (vy < -0.5)
|
|
106
|
+
landed = true;
|
|
107
|
+
if (landed && vy > rose)
|
|
108
|
+
rose = vy;
|
|
109
|
+
}
|
|
110
|
+
assert(rose <= 0.3, `a ball with restitution 0 must not bounce, it rose at ${rose} m/s`);
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "a static body never moves and a kinematic body moves only by pose writes",
|
|
115
|
+
run({ facility }) {
|
|
116
|
+
facility.addBody("wall", pose(0, 1, 0), { state: "static" }, { kind: "box", dimensions: [1, 1, 1] });
|
|
117
|
+
facility.addBody("lift", pose(2, 1, 0), { state: "kinematic" }, { kind: "sphere", dimensions: [BALL_RADIUS, 0, 0] });
|
|
118
|
+
steps(facility, 60);
|
|
119
|
+
assert(close(facility.getBodyPose("wall").position, [0, 1, 0], HOLD_EPS), "a static body must not fall");
|
|
120
|
+
assert(close(facility.getBodyPose("lift").position, [2, 1, 0], HOLD_EPS), "a kinematic body must not fall");
|
|
121
|
+
assert(facility.getBodyState("lift") === "kinematic", "the state reads back as kinematic");
|
|
122
|
+
facility.setBodyPose("lift", pose(2, 2, 0));
|
|
123
|
+
steps(facility, 10);
|
|
124
|
+
assert(close(facility.getBodyPose("lift").position, [2, 2, 0], HOLD_EPS), "a kinematic body follows a pose write and stays");
|
|
125
|
+
facility.setBodyState("lift", "dynamic");
|
|
126
|
+
steps(facility, 30);
|
|
127
|
+
assert(facility.getBodyPose("lift").position[1] < 2 - 0.5, "once dynamic, the same body falls");
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "suspend holds the body still and pose writes stick exactly, until resume",
|
|
132
|
+
run({ facility }) {
|
|
133
|
+
ball(facility);
|
|
134
|
+
facility.suspend("ball");
|
|
135
|
+
assert(facility.isSuspended("ball"), "isSuspended reports the hold");
|
|
136
|
+
steps(facility, 30);
|
|
137
|
+
assert(close(facility.getBodyPose("ball").position, [0, DROP_HEIGHT, 0], HOLD_EPS), `a suspended body must not fall, it is at ${JSON.stringify(facility.getBodyPose("ball").position)}`);
|
|
138
|
+
facility.setBodyPose("ball", pose(0.5, 2, 0.25));
|
|
139
|
+
steps(facility, 30);
|
|
140
|
+
assert(close(facility.getBodyPose("ball").position, [0.5, 2, 0.25], HOLD_EPS), "a pose written to a suspended body sticks exactly");
|
|
141
|
+
facility.suspend("ball");
|
|
142
|
+
assert(facility.isSuspended("ball"), "suspending twice is still one hold");
|
|
143
|
+
facility.resume("ball", { linearVelocity: [0, 0, 0], angularVelocity: [0, 0, 0] });
|
|
144
|
+
assert(!facility.isSuspended("ball"), "resume ends the hold");
|
|
145
|
+
assert(facility.getBodyState("ball") === "dynamic", "resume restores the state the body had");
|
|
146
|
+
steps(facility, 30);
|
|
147
|
+
assert(facility.getBodyPose("ball").position[1] < 2 - 0.5, "a resumed body falls again");
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "resume with a release velocity throws the body, and with zeros lets it rest",
|
|
152
|
+
run({ facility }) {
|
|
153
|
+
ball(facility, 0, 0);
|
|
154
|
+
facility.suspend("ball");
|
|
155
|
+
facility.resume("ball", { linearVelocity: [2, 0, 0], angularVelocity: [0, 0, 0] });
|
|
156
|
+
facility.step(DT);
|
|
157
|
+
const moved = facility.getBodyPose("ball").position[0];
|
|
158
|
+
assert(moved > 2 * DT * 0.5, `a release at 2 m/s must move the body that way within one step, moved ${moved}`);
|
|
159
|
+
facility.resume("ball", { linearVelocity: [0, 0, 0], angularVelocity: [0, 0, 0] });
|
|
160
|
+
const after = facility.getBodyPose("ball").position[0];
|
|
161
|
+
facility.step(DT);
|
|
162
|
+
assert(Math.abs(facility.getBodyPose("ball").position[0] - after) > 2 * DT * 0.5, "resuming a body that is not suspended changes nothing: it keeps flying");
|
|
163
|
+
facility.suspend("ball");
|
|
164
|
+
facility.resume("ball", { linearVelocity: [0, 0, 0], angularVelocity: [0, 0, 0] });
|
|
165
|
+
const rest = facility.getBodyPose("ball").position;
|
|
166
|
+
steps(facility, 10);
|
|
167
|
+
assert(close(facility.getBodyPose("ball").position, rest, HOLD_EPS), "a zero release rests the body (no gravity here, gravity factor 0)");
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "setBodyPose while not suspended teleports the body and clears its velocity",
|
|
172
|
+
run({ facility }) {
|
|
173
|
+
ball(facility);
|
|
174
|
+
facility.setVelocity("ball", { linear: [50, 0, 0], angular: [0, 0, 0] });
|
|
175
|
+
facility.step(DT);
|
|
176
|
+
assert(facility.getBodyPose("ball").position[0] > 0.5, "a body given 50 m/s moves in one step");
|
|
177
|
+
const target = pose(-1, 3, 0.5);
|
|
178
|
+
facility.setBodyPose("ball", target);
|
|
179
|
+
facility.step(DT);
|
|
180
|
+
const at = facility.getBodyPose("ball").position;
|
|
181
|
+
// One step of gravity from rest is about 1.4 mm; a kept velocity would be 0.83 m away.
|
|
182
|
+
assert(close(at, target.position, REST_EPS), `a teleported body must be at the pose one step later, not carrying its old velocity; it is at ${JSON.stringify(at)}`);
|
|
183
|
+
assert(speed(facility.getVelocity("ball").linear) < 1, "a teleport clears the velocity");
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: "setVelocity and getVelocity round-trip, and every tuple handed back is fresh",
|
|
188
|
+
run({ facility }) {
|
|
189
|
+
ball(facility, 0, 0);
|
|
190
|
+
facility.setVelocity("ball", { linear: [1, 2, 3], angular: [0, 0.5, 0] });
|
|
191
|
+
const read = facility.getVelocity("ball");
|
|
192
|
+
assert(close(read.linear, [1, 2, 3], 1e-3) && close(read.angular, [0, 0.5, 0], 1e-3), `velocity must read back as set, got ${JSON.stringify(read)}`);
|
|
193
|
+
read.linear[0] = 99;
|
|
194
|
+
assert(facility.getVelocity("ball").linear[0] !== 99, "the tuple handed back must be the caller's own copy");
|
|
195
|
+
const at = facility.getBodyPose("ball");
|
|
196
|
+
at.position[0] = 99;
|
|
197
|
+
assert(facility.getBodyPose("ball").position[0] !== 99, "the pose handed back must be the caller's own copy");
|
|
198
|
+
const g = facility.getGravity();
|
|
199
|
+
g[1] = 0;
|
|
200
|
+
assert(facility.getGravity()[1] !== 0, "the gravity handed back must be the caller's own copy");
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "gravity factor 0 floats, and setGravity changes the world for every dynamic body",
|
|
205
|
+
run({ facility }) {
|
|
206
|
+
ball(facility, 0, 0);
|
|
207
|
+
steps(facility, 60);
|
|
208
|
+
assert(close(facility.getBodyPose("ball").position, [0, DROP_HEIGHT, 0], HOLD_EPS), "a body with gravity factor 0 floats");
|
|
209
|
+
facility.addBody("stone", pose(1, DROP_HEIGHT, 0), undefined, { kind: "sphere", dimensions: [BALL_RADIUS, 0, 0] });
|
|
210
|
+
facility.setGravity([0, 0, 0]);
|
|
211
|
+
assert(close(facility.getGravity(), [0, 0, 0], 1e-6), "setGravity reads back");
|
|
212
|
+
steps(facility, 60);
|
|
213
|
+
assert(close(facility.getBodyPose("stone").position, [1, DROP_HEIGHT, 0], HOLD_EPS), "with no gravity a dynamic body floats too");
|
|
214
|
+
facility.setGravity([0, -9.81, 0]);
|
|
215
|
+
steps(facility, 60);
|
|
216
|
+
assert(facility.getBodyPose("stone").position[1] < DROP_HEIGHT - 0.5, "with gravity back it falls");
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "dispose removes every body",
|
|
221
|
+
run({ facility }) {
|
|
222
|
+
floor(facility);
|
|
223
|
+
ball(facility);
|
|
224
|
+
facility.dispose();
|
|
225
|
+
assert(!facility.hasBody("ball") && !facility.hasBody("floor"), "dispose must remove every body");
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
//# sourceMappingURL=physics-contract-cases.js.map
|