@displayxr/inline3d 1.43.1 → 1.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,135 @@ entry points (`.`, `./three`) are frozen for 1.x, while the **scene subpaths** (
5
5
  `./splat`, `./model`) are a preview tier whose options may change in any release. Entries below say
6
6
  which tier they touch, because that is what tells you whether an upgrade can move your pixels.
7
7
 
8
+ ## 1.45.0 — 2026-10-13
9
+
10
+ ### Packaging
11
+
12
+ - `js/inline3d-cursor-shape.js` (new in this release, imported by `./cursor-depth` and so by
13
+ `./three`) is now in the published file set. A new unit test fails whenever a published module
14
+ imports a file the package does not ship.
15
+
16
+ ### Depth cursor: the sprite is the page's CSS cursor — any keyword, and `url()` images (ADR-046 §C, page side)
17
+
18
+ Touches `./cursor-depth`, `./three` (`DepthCursor`) and the preview tier (`./model`, `./splat`,
19
+ `./call`, `./camera`), all only under `cursor: 'depth'`.
20
+
21
+ - The lifted cursor was always the arrow, so over a model whose page cursor is `grab` it showed an
22
+ arrow. It now draws what the browser would show: the page's computed CSS cursor under the pointer.
23
+ - **Every standard keyword** has a procedural sprite in the arrow's style (white fill, dark
24
+ outline, the OS cursor's 19 CSS px, its hotspot at its natural point): `pointer`, `grab`,
25
+ `grabbing`, `move` / `all-scroll`, `text` / `vertical-text`, `crosshair`, `not-allowed` /
26
+ `no-drop`, `help`, `wait`, `progress`, `copy`, `alias`, `context-menu`, `cell`, every resize
27
+ cursor (`n/s/e/w/ne/nw/se/sw-resize`, `ns/ew/nesw/nwse-resize`, `col-resize`, `row-resize`),
28
+ `zoom-in`, `zoom-out`; `auto` / `default` and anything unknown → the arrow.
29
+ - **`none`** draws nothing and keeps the OS cursor hidden: a page that hides its cursor on purpose
30
+ stays that way.
31
+ - **`url(a) x y, url(b), fallback`**: the first url that loads as an image (crossOrigin
32
+ 'anonymous' — same-origin and `data:` always do; cross-origin without CORS falls to the next
33
+ item), at its natural CSS size capped at 128 px; hotspot = the explicit `x y`, else a `.cur`
34
+ file's own, else 0 0. Loaded async and cached per URL; the keyword fallback is drawn until it is
35
+ ready.
36
+ - All of it softened like the arrow, in the cursor's own material (keyword sprites and images are
37
+ textures on one soft quad: one shader program, the same 13-tap blur and opacity). No CSS filter,
38
+ no CSS opacity.
39
+ - **`cursor: { shape }`**: `'auto'` (default, mirror the CSS), `'none'`, or a sprite name
40
+ (`CURSOR_SHAPES`) to fix it. **`cursor: { shapeFor(hit) }`**: decide per drawn frame
41
+ (`{ u, v, point, css, cssValue }` → a name, or nothing for `shape`'s answer).
42
+ - **Behaviour change — the CSS hide.** The cursor no longer writes `canvas.style.cursor = 'none'`
43
+ while it draws: it sets a `data-inline3d-cursor-hidden` attribute (on the canvas, or on `<html>`
44
+ for `pointerScope: 'window'`, replacing the `inline3d-cursor-hidden` class) plus one injected
45
+ `cursor: none !important` rule. The page's own cursor value is never touched, so it can be read
46
+ back (the attribute is lifted for the read and put back in the same task, so nothing paints).
47
+ Reads happen only on frames that draw the sprite, after pointer moves (at most every 100 ms) and
48
+ at once on pointerdown / pointerup. A page that checked `style.cursor === 'none'` should check
49
+ the attribute (or `getComputedStyle(canvas).cursor`).
50
+ - `tools/cursor-shape-capture/run.mjs`: headless per-sprite captures over the lifted model sample
51
+ (keywords, a `data:` url() image, `none`), hotspot stability and the Shop grab → grabbing → grab
52
+ sequence, failing on any GL error; the samples take `&shape=`.
53
+
54
+ ### Fixed — `cursor: 'depth'` (PlayCanvas) over a layer on the display rig (#185)
55
+
56
+ - A layer moved with `handle.setLayerRig(name, 'display')` is drawn by the layer rig's cameras, not
57
+ the eye camera, so the pick never saw it and the cursor stayed on the glass over it (a round
58
+ stage, a map tile under a camera-rig photo). The layer rig's post run, and its display run when
59
+ it is not rounded, now join the eye pick. A ROUNDED display run (a camera rig, gain ≠ 1) gets a
60
+ second low-res pick pass through the same round views, and each hit becomes the point the photo
61
+ views show at the same pixels in both eyes, so the placement (unchanged) puts the arrow in front
62
+ of the stage as the panel shows it. The two passes merge by the nearest point. Same cadence and
63
+ readback as before (every other frame, asynchronous); the second pass exists only while a layer
64
+ is on the display rig, and nothing at all without `cursor: 'depth'`.
65
+
66
+ ### Added — a `pc.ShaderMaterial` can take part in the depth pick
67
+
68
+ - `CURSOR_DEPTH_PICK_GLSL` and `CURSOR_DEPTH_PICK_WGSL` (`./cursor-depth`): `{ declarations, output }`
69
+ snippets that write the pick id and, under `DEPTH_PICK_PASS`, the pick depth with the engine's own
70
+ `pickPS` chunk. Since 1.44.0 the pick pass leaves out a ShaderMaterial that does not write the
71
+ depth target; with these it is hit like any engine material. docs/authoring-inline-3d.md §A cursor
72
+ that respects depth.
73
+
74
+ ## 1.44.0 — 2026-10-13
75
+
76
+ Touches the **core** `./three` (`DepthCursor`), `./cursor-depth`, and the **preview tier** (`./model`,
77
+ `./splat`, `./call`, `./camera`: `cursor: 'depth'`). **One deliberate look change:** a LIFTED depth
78
+ cursor is now softened by default. On the glass nothing changes (nothing is drawn there).
79
+
80
+ ### Depth cursor responsiveness (`./model`, three.js)
81
+
82
+ - The arrow follows the **newest** pointer position every frame, and the pick is amortised (3 rays a
83
+ frame) instead of casting the whole footprint synchronously.
84
+ - BVH pick: three-mesh-bvh 0.9.15 (MeshBVH only, MIT) is vendored at `js/vendor/three-mesh-bvh.js`
85
+ and loaded **only** with `cursor: 'depth'`. Nothing on the page is mutated. Skinned, morphing,
86
+ instanced and batched meshes, and partial draw ranges, keep three's own raycast.
87
+ - The pointer comes from `pointerrawupdate` (coalesced samples) where the browser has it.
88
+ - CarConcept, headless: `update()` 8.2 → 0.16 ms mean, 50.6 → 0.5 ms max.
89
+ - New `cursor: { debug: true | 'both' }`: rolling cost stats; `'both'` keeps the OS cursor visible
90
+ beside the sprite (the latency check).
91
+ - If you vendor the SDK, copy `js/vendor/` too. The BVH slice imports bare `three`.
92
+
93
+ ### Depth cursor: crosstalk softening of the lifted arrow (runtime ADR-046 Amendment 2 §B.7 parity)
94
+
95
+ - On a lenticular panel a lifted, white, sharp-edged arrow ghosts: crosstalk leaks a copy one
96
+ disparity away, and its visibility scales with contrast × edge sharpness. The runtime softens its
97
+ cursor by lift; every web depth cursor now does the same, constant for constant, so web and
98
+ native cursors look alike: an edge blur of 0.1 × the ghost offset (the neighbouring views' split,
99
+ panel px), capped at 6% of the arrow's drawn height and 8 px, and an opacity easing from 1 to 0.5
100
+ with a smoothstep over 0.03 eye baselines of lift. Both are exactly zero on the glass, and the
101
+ tip (the click point) never moves. Defaults tuned on a Windows Leia panel and confirmed on a Mac.
102
+ - **`cursor.soften`** on `./model`, `./splat`, `./call`, `./camera`, and `soften` on `DepthCursor`:
103
+ `'both'` (default), `'off'` (the crisp arrow, pixel-identical to 1.43), `'alpha'`, `'blur'`, or
104
+ `{ mode, blurK, blurMaxPx, blurMaxFrac, alphaMin, fullD }` over the defaults. Validated at the call.
105
+ - Drawn in the cursor's own material: a quad padded by the blur radius whose fragment shader
106
+ evaluates the arrow analytically at the runtime's 13 blur taps (three and PlayCanvas), or the
107
+ same function rasterised into the woven buffer (stereo pairs). Never a CSS `filter` or `opacity`.
108
+ A frame with nothing to soften draws the crisp mesh as before; zero cost without `cursor: 'depth'`.
109
+ - `./cursor-depth` exports the pieces: `cursorSoften`, `cursorGhostPx`, `cursorProjectToCanvas`,
110
+ `cursorPlacementSoften`, `cursorSoftArrowGLSL`, `cursorSoftArrowRaster`, `cursorSoftArrowSample`,
111
+ `resolveCursorSoften`, `parseCursorSoften`, `CURSOR_SOFTEN_DEFAULTS`, `CURSOR_SOFTEN_KERNEL`, and
112
+ more. The samples take `&soften=both|off|alpha|blur` next to `?cursor=depth`.
113
+ - docs/authoring-inline-3d.md §A cursor that respects depth.
114
+
115
+ ### Fixed — `cursor: 'depth'` (PlayCanvas): no GL error storm from an app's own `ShaderMaterial`
116
+
117
+ - The depth pick pass renders into two colour targets (pick id + pick depth). A mesh whose
118
+ `pc.ShaderMaterial` writes only one output (a custom backdrop, frame or chrome quad) had every
119
+ pick draw rejected with `GL_INVALID_OPERATION: Active draw buffers with missing fragment shader
120
+ outputs` — hundreds a second on a panel, until Chromium stopped reporting them. Such meshes are now
121
+ left out of the pick pass (they never reached the pick buffer anyway), so nothing the cursor sees
122
+ changes. Materials that handle `DEPTH_PICK_PASS`, followed through their `#include`s (the
123
+ engine's gsplat material included), stay in.
124
+
125
+ ### Fixed — the depth cursor drifted off a still pointer while the camera moved
126
+
127
+ - With `cursor: 'depth'` and the default `anchor: 'hybrid'`, a still pointer's arrow was pinned in
128
+ the SCENE, so anything that moved the camera rig under it carried it away: `./model`'s idle
129
+ turntable (8°/s) put it ~0.26 of the canvas off the pointer within ~6 s, and a damped drag or a
130
+ scripted camera did the same. The kept line of sight now lives in the display's frame (relative
131
+ to the canvas, in canvas heights), as in the runtime's `u_cursor_depth`, which works in display
132
+ space: a rig orbit, pan or zoom leaves the arrow on the pointer, and a head move still
133
+ parallaxes it like content. A canvas whose shape changes under a still pointer (a resize)
134
+ re-anchors, as does the head pulling back past the anchor eye. `'screen'` and `'world'` are
135
+ unchanged (neither keeps state across frames).
136
+
8
137
  ## 1.43.1 — 2026-10-13
9
138
 
10
139
  ### Fixed — `warmEntity` on a display-rig layer
@@ -1,13 +1,15 @@
1
1
  # Third-party notices — `@displayxr/inline3d`
2
2
 
3
- `@displayxr/inline3d` is Apache-2.0 (see `LICENSE`). It bundles no third-party code, but parts of
4
- it are adapted from, or run on, the MIT-licensed projects below. Their notices follow.
3
+ `@displayxr/inline3d` is Apache-2.0 (see `LICENSE`). It bundles one piece of third-party code —
4
+ a slice of three-mesh-bvh, below — and parts of it are adapted from, or run on, the MIT-licensed
5
+ projects below. Their notices follow.
5
6
 
6
7
  | Project | Version | How it is used |
7
8
  |---|---|---|
8
9
  | [PlayCanvas engine](https://github.com/playcanvas/engine) | 2.22.3 | Optional peer (`playcanvas`): the default splat backend and the `./model` backend. Shader code in this package is **adapted** from it: the non-square-pixel footprint rewrite of `src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatCorner.js` (`js/inline3d-splat-playcanvas.js`), the quad-extent cap in `gsplatCommon.js` (`js/inline3d-splat-perf.js`), and the dissolve effect and its hash/noise/fbm helpers from `scripts/esm/gsplat/shader-effect-dissolve.mjs` (`js/inline3d-splat-effects.js`). Each adapted site carries a provenance comment. |
9
10
  | [three.js](https://github.com/mrdoob/three.js) | ≥ 0.150 | Optional peer (`three`): the `./three` helpers, the Spark splat path, `./model`'s `engine: 'three'`. The PlayCanvas backend's `environment: 'room'` is **adapted** from `examples/jsm/environments/RoomEnvironment.js` (its scene numbers, re-rendered by ray casting in `js/inline3d-model-playcanvas.js`, `ROOM_ENVIRONMENT`). |
10
11
  | [Spark](https://github.com/sparkjsdev/spark) | ≥ 2.0 | Optional peer (`@sparkjsdev/spark`): the Spark splat backend. |
12
+ | [three-mesh-bvh](https://github.com/gkjohnson/three-mesh-bvh) | 0.9.15 | **Bundled**: `js/vendor/three-mesh-bvh.js` is its `MeshBVH`, minified by `tools/vendor/three-mesh-bvh.mjs` (three stays external). Loaded only when a page opts into `cursor: 'depth'` on `./model`'s three.js engine, for the cursor's BVH pick (`js/inline3d-cursor-bvh.js`). |
11
13
 
12
14
  ## PlayCanvas engine — MIT
13
15
 
@@ -75,6 +77,28 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
75
77
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
76
78
  THE SOFTWARE.
77
79
 
80
+ ## three-mesh-bvh — MIT
81
+
82
+ Copyright (c) 2018 Garrett Johnson
83
+
84
+ Permission is hereby granted, free of charge, to any person obtaining a copy
85
+ of this software and associated documentation files (the "Software"), to deal
86
+ in the Software without restriction, including without limitation the rights
87
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
88
+ copies of the Software, and to permit persons to whom the Software is
89
+ furnished to do so, subject to the following conditions:
90
+
91
+ The above copyright notice and this permission notice shall be included in all
92
+ copies or substantial portions of the Software.
93
+
94
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
95
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
96
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
97
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
98
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
99
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
100
+ SOFTWARE.
101
+
78
102
  ## Sample assets (repo samples only, not in the npm package)
79
103
 
80
104
  The pages under `samples/` show these assets. None is in the published `@displayxr/inline3d`
package/camera.d.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  // until the snapshot is updated in the same change (`node tools/api-surface.mjs --update`).
13
13
 
14
14
  import type { Inline3D, Inline3DUnsupported, TileHandle } from './index.js';
15
+ import type { CursorShape, CursorShapeHit, CursorShapeOption, CursorSoftenOption } from './cursor-depth.js';
15
16
 
16
17
  /** What the camera delivers: a side-by-side pair (left eye left) or one flat picture. */
17
18
  export type CameraFormat = 'sbs' | 'mono';
@@ -193,6 +194,12 @@ export interface PairCursorOptions {
193
194
  height?: number;
194
195
  /** `'window'` (default): the cursor stays while the pointer is over DOM on the tile (a badge). `'canvas'`: only over the canvas itself. */
195
196
  pointerScope?: 'canvas' | 'window';
197
+ /** Crosstalk softening of the lifted arrow (runtime ADR-046 Amendment 2 §B.7), drawn into the woven pixels. Default `'both'`. */
198
+ soften?: CursorSoftenOption;
199
+ /** The sprite. Default `'auto'`: the page's CSS cursor over the tile (any keyword, url() images, `none`). */
200
+ shape?: CursorShapeOption;
201
+ /** Per-frame override while the sprite draws (`point` is null on a pair): a shape name, or nothing for `shape`'s. */
202
+ shapeFor?: (hit: CursorShapeHit) => CursorShape | 'none' | 'auto' | null | undefined;
196
203
  }
197
204
 
198
205
  export interface CameraViewOptions {
package/cursor-depth.d.ts CHANGED
@@ -66,7 +66,9 @@ export function placeCursor(g: CursorGeometry, disparity: number, heightFraction
66
66
  /**
67
67
  * Where along the cursor's depth the sprite goes.
68
68
  * - `'hybrid'` (default): on the line of sight while the pointer MOVES (exactly over it), and
69
- * world-FIXED while it is still, so it parallaxes with the content as the head moves.
69
+ * fixed relative to the DISPLAY while it is still, so it parallaxes with the content as the head
70
+ * moves. Anchored in the display's frame, not the scene's: a rig that orbits, pans or zooms
71
+ * (an idle spin, a damped drag, a scripted camera) does not move a still cursor off the pointer.
70
72
  * - `'screen'`: always on the line of sight. Never parallaxes; on a head-tracked display it can
71
73
  * read as "at the glass".
72
74
  * - `'world'`: always straight in front of the pointer's canvas point. Parallaxes; drifts ~mm off
@@ -85,6 +87,19 @@ export interface CursorOptions {
85
87
  anchor?: CursorAnchorMode;
86
88
  /** `'window'`: keep the cursor over DOM layered on the canvas, and hide the CSS cursor page-wide meanwhile. Default `'canvas'`. */
87
89
  pointerScope?: 'canvas' | 'window';
90
+ /**
91
+ * Diagnostics, off by default and free when off. `true`: log the cursor's per-frame cost
92
+ * (update ms, rays a frame, frame interval, pointer-sample age) as rolling stats, at most once a
93
+ * second. `'both'`: that, and the OS cursor stays visible beside the sprite, for filming the lag
94
+ * between them on a panel. three.js backend (./model `engine: 'three'`, ./splat on Spark).
95
+ */
96
+ debug?: boolean | 'both';
97
+ /** Crosstalk softening of the lifted arrow (runtime ADR-046 Amendment 2 §B.7). Default `'both'`. */
98
+ soften?: CursorSoftenOption;
99
+ /** The sprite. Default `'auto'`: mirror the page's CSS cursor under the pointer (`grab` → the open hand, …). */
100
+ shape?: CursorShapeOption;
101
+ /** Per-frame override while the sprite draws: a shape name, or nothing for `shape`'s answer. */
102
+ shapeFor?: (hit: CursorShapeHit) => CursorShape | 'none' | 'auto' | null | undefined;
88
103
  }
89
104
  export const CURSOR_OPTION_KEYS: readonly (keyof CursorOptions)[];
90
105
  /** `cursor` as ./model and ./splat accept it: null = off; throws on a bad value. */
@@ -152,3 +167,262 @@ export class CursorDepthPlacer {
152
167
  nowSec: number,
153
168
  ): CursorPlacement;
154
169
  }
170
+
171
+ // ── The sprite: the page's CSS cursor, mirrored (ADR-046 Amendment 2 §C, page side) ─────────
172
+
173
+ /** The procedural sprites: one per standard CSS cursor keyword (aliases share one). */
174
+ export type CursorShape =
175
+ | 'arrow' | 'pointer' | 'grab' | 'grabbing' | 'move' | 'text' | 'vertical-text' | 'crosshair'
176
+ | 'not-allowed' | 'help' | 'wait' | 'progress' | 'copy' | 'alias' | 'context-menu' | 'cell'
177
+ | 'ns-resize' | 'ew-resize' | 'nesw-resize' | 'nwse-resize' | 'col-resize' | 'row-resize'
178
+ | 'zoom-in' | 'zoom-out';
179
+ /** `cursor.shape`: `'auto'` (default — the page's computed CSS cursor, url() images included), `'none'`, or a sprite. */
180
+ export type CursorShapeOption = 'auto' | 'none' | CursorShape;
181
+ /** What `shapeFor` is handed each drawn frame. */
182
+ export interface CursorShapeHit {
183
+ /** Canvas-normalised pointer, v down. */
184
+ u: number;
185
+ v: number;
186
+ /** The nearest content point under the footprint (the views' / world space), null on a pair or a miss. */
187
+ point: number[] | null;
188
+ /** What `'auto'` would draw: a sprite name, `'none'` or `'image'` (null unless `shape: 'auto'`). */
189
+ css: CursorShape | 'none' | 'image' | null;
190
+ /** The page's computed CSS `cursor` value (null unless `shape: 'auto'`). */
191
+ cssValue: string | null;
192
+ }
193
+ /** What a cursor draws: nothing, a keyword sprite, or a loaded url() image. */
194
+ export type CursorSprite =
195
+ | { kind: 'none' }
196
+ | { kind: 'shape'; name: CursorShape; key: string }
197
+ | { kind: 'image'; key: string; entry: CursorImageEntry; width: number; height: number; hotspot: [number, number] };
198
+ export interface CursorImageEntry {
199
+ url: string;
200
+ state: 'loading' | 'ready' | 'failed';
201
+ image: HTMLImageElement | null;
202
+ /** Natural size, CSS px. */
203
+ width: number;
204
+ height: number;
205
+ /** A .cur file's own hotspot, once read. */
206
+ curHotspot: [number, number] | null;
207
+ }
208
+ export const CURSOR_SHAPES: readonly CursorShape[];
209
+ export const CURSOR_SHAPE_OPTIONS: readonly CursorShapeOption[];
210
+ /** Every CSS cursor keyword the map knows. */
211
+ export const CURSOR_CSS_KEYWORDS: readonly string[];
212
+ /** The data attribute that hides the CSS cursor while the sprite draws (on the canvas; on <html> for pointerScope 'window'). */
213
+ export const CURSOR_HIDDEN_ATTR: string;
214
+ /** The one rule injected for it: `cursor: none !important` on the attribute's element and its descendants. */
215
+ export const CURSOR_HIDDEN_RULE: string;
216
+ /** Pointer moves re-read the CSS cursor at most this often, ms (100); press/release re-read at once. */
217
+ export const CURSOR_CSS_READ_MS: number;
218
+ /** Image cursors larger than this (CSS px) are scaled down to it (128). */
219
+ export const CURSOR_IMAGE_MAX_PX: number;
220
+ /** Texels per sprite unit of the keyword textures (64). */
221
+ export const CURSOR_SHAPE_TEXTURE_PX: number;
222
+ /** One keyword → its sprite, `'none'`, or the arrow for anything unknown. */
223
+ export function cssKeywordToShape(keyword: string): CursorShape | 'none';
224
+ /** A computed CSS `cursor` value's keyword fallback → its sprite (images ignored). */
225
+ export function cssCursorToShape(css: string | null | undefined): CursorShape | 'none';
226
+ /** A computed CSS `cursor` value: its url() images in order (explicit hotspot or null) and the keyword fallback. */
227
+ export function parseCssCursor(css: string | null | undefined): { images: ReadonlyArray<{ url: string; hotspot: [number, number] | null }>; keyword: string };
228
+ /** `shapeFor`'s answer if it names a sprite (or 'none'), else the fixed `shape`, else the CSS one, else the arrow. */
229
+ export function cursorPickShape(shape: CursorShapeOption | undefined, fromShapeFor: unknown, css: string | null | undefined): CursorShape | 'none';
230
+ /** A .cur file's hotspot (the entry `w` px wide, else the first), or null when not a cursor file. */
231
+ export function curFileHotspot(buf: ArrayBuffer | null, w?: number): [number, number] | null;
232
+ /** Cursor images by URL (crossOrigin 'anonymous'), loaded once. */
233
+ export class CursorImageCache {
234
+ constructor(opts?: { ImageCtor?: unknown; fetchFn?: unknown });
235
+ get(url: string): CursorImageEntry;
236
+ }
237
+ /** The page-wide cache the cursors share. */
238
+ export const cursorImageCache: CursorImageCache;
239
+ /** What to draw for a computed CSS cursor value: the first READY image (failed → next, loading → the fallback), else the keyword. */
240
+ export function resolveCursorSprite(css: string, images?: Pick<CursorImageCache, 'get'>): CursorSprite;
241
+ export function spriteForName(name: CursorShape | 'none' | string): CursorSprite;
242
+ /** The sprite's area in sprite units (arrow heights; y down, hotspot at the origin). */
243
+ export function cursorSpriteRect(sprite: CursorSprite): [number, number, number, number];
244
+ /** That area padded by `radius` (+ `aa`). */
245
+ export function cursorSpriteBox(sprite: CursorSprite, radius: number, aa?: number): [number, number, number, number];
246
+ /** A sprite's geometry (sprite units, y down, hotspot at the origin). */
247
+ export interface CursorShapeGeometry {
248
+ readonly name: CursorShape;
249
+ /** The dark outline: a union of polygons. */
250
+ readonly parts: ReadonlyArray<ReadonlyArray<readonly [number, number]>>;
251
+ /** Each part inset by 1/19: the white fill. */
252
+ readonly fill: ReadonlyArray<ReadonlyArray<readonly [number, number]>>;
253
+ /** Dark marks over the fill (the ? of help, the + of copy). */
254
+ readonly detail: ReadonlyArray<ReadonlyArray<readonly [number, number]>>;
255
+ readonly box: readonly [number, number, number, number];
256
+ }
257
+ export function cursorShape(name: CursorShape | string): CursorShapeGeometry;
258
+ type CursorPolys = { outline: ReadonlyArray<ReadonlyArray<readonly [number, number]>>; fill: ReadonlyArray<ReadonlyArray<readonly [number, number]>>; detail?: ReadonlyArray<ReadonlyArray<readonly [number, number]>> };
259
+ /** The outline, the fill inset by `d` sprite units (default 1/19), the detail. */
260
+ export function cursorShapePolys(name: CursorShape | string, d?: number): CursorPolys;
261
+ /** The shape's box padded by `radius` (+ `aa`). */
262
+ export function cursorShapeBox(name: CursorShape | string, radius: number, aa?: number): [number, number, number, number];
263
+ /** The softened shape at (x, y) of sprite units (straight RGB + alpha); for the arrow, exactly cursorSoftArrowSample. */
264
+ export function cursorShapeSample(name: CursorShape | string, x: number, y: number, radius: number, aa: number, fill?: readonly number[], outline?: readonly number[], polys?: CursorPolys): [number, number, number, number];
265
+ /** The softened shape rasterised for a 2D canvas, the hotspot at (cx, cy), one sprite unit = `sizePx`. */
266
+ export function cursorShapeRaster(name: CursorShape | string, cx: number, cy: number, sizePx: number, radius: number, alpha: number, fill?: readonly number[], outline?: readonly number[], polys?: CursorPolys): { x0: number; y0: number; width: number; height: number; data: Uint8ClampedArray };
267
+ /** A keyword sprite as a crisp straight-alpha RGBA texture (row 0 = the top) and the sprite-unit rect it covers. */
268
+ export function cursorShapeTexture(name: CursorShape | string): { width: number; height: number; data: Uint8ClampedArray; rect: readonly [number, number, number, number] };
269
+ /** RGBA pixels softened with the same 13-tap kernel and opacity, padded by the radius. */
270
+ export function cursorSoftenPixels(src: ArrayLike<number>, w: number, h: number, radiusPx: number, alpha: number): { pad: number; width: number; height: number; data: Uint8ClampedArray };
271
+ /** The crisp shape drawn with a 2D context (outline dark, fill white, detail dark), the hotspot at (cx, cy). */
272
+ export function drawCursorShape2D(g: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, name: CursorShape | string, cx: number, cy: number, sizePx: number, polys?: CursorPolys, fillStyle?: string, outlineStyle?: string): void;
273
+ /** GLSL defining `vec4 inline3dSoftSprite(sampler2D tex, vec2 p, vec4 rect, float radius)` — every non-arrow sprite, one program. */
274
+ export function cursorSpriteGLSL(): string;
275
+ /** Inject CURSOR_HIDDEN_RULE once; false without a document. */
276
+ export function ensureCursorHiddenRule(doc?: Document): boolean;
277
+ /** The page's computed cursor at `el` with the depth cursor's hide lifted (synchronously) off `holders`; null if unreadable. */
278
+ export function readPageCursor(el: Element, holders: ReadonlyArray<Element | null | undefined>): string | null;
279
+ /** Tracks the page's CSS cursor under the pointer: listeners mark it stale; `value()` re-reads (moves throttled, press/release at once). */
280
+ export class CursorCssReader {
281
+ constructor(pointer: { canvas: Element; scope: 'canvas' | 'window' }, opts?: { throttleMs?: number });
282
+ /** The last computed CSS cursor read, or null. */
283
+ readonly css: string | null;
284
+ /** Reads performed (diagnostics). */
285
+ readonly reads: number;
286
+ throttleMs: number;
287
+ value(nowMs?: number): string;
288
+ shape(nowMs?: number): CursorShape | 'none';
289
+ read(nowMs?: number): string;
290
+ dispose(): void;
291
+ }
292
+ /** Per-cursor sprite state: the option, `shapeFor`, and a reader only for `'auto'`. */
293
+ export class CursorShapeSelector {
294
+ constructor(pointer: { canvas: Element; scope: 'canvas' | 'window' }, opts?: { shape?: CursorShapeOption; shapeFor?: ((hit: CursorShapeHit) => unknown) | null; images?: Pick<CursorImageCache, 'get'> });
295
+ readonly option: CursorShapeOption;
296
+ readonly reader: CursorCssReader | null;
297
+ /** The sprite picked last. */
298
+ readonly sprite: CursorSprite;
299
+ /** …as a label: a sprite name, 'none' or 'image'. */
300
+ readonly current: CursorShape | 'none' | 'image';
301
+ pick(hit: { u: number; v: number; point: number[] | null }, nowMs?: number): CursorSprite;
302
+ dispose(): void;
303
+ }
304
+
305
+ // ── Crosstalk softening of the lifted cursor (runtime ADR-046 Amendment 2 §B.7) ─────────────
306
+
307
+ /**
308
+ * `'both'` (default): edge blur AND opacity easing; `'off'`: neither (the crisp arrow, exactly as
309
+ * before 1.44); `'alpha'` / `'blur'`: one of them (A/B presets).
310
+ */
311
+ export type CursorSoftenPreset = 'both' | 'off' | 'alpha' | 'blur';
312
+ /** The tuning knobs, each over the runtime's default (U_CURSOR_SOFTEN_DEFAULT_*). */
313
+ export interface CursorSoftenTuning {
314
+ /** Which effects; default `'both'`. */
315
+ mode?: CursorSoftenPreset;
316
+ /** Panel px of blur per panel px of ghost offset (the views' on-screen split). Default 0.1; >= 0. */
317
+ blurK?: number;
318
+ /** Hard ceiling on the blur radius, panel px. Default 8; [0, 8]. */
319
+ blurMaxPx?: number;
320
+ /** Blur cap as a fraction of the drawn arrow's height. Default 0.06; [0, 0.5] (0 = off). */
321
+ blurMaxFrac?: number;
322
+ /** Opacity at full lift. Default 0.5; [0, 1]. */
323
+ alphaMin?: number;
324
+ /** |disparity| (eye baselines) at which the opacity reaches alphaMin. Default 0.03; > 0. */
325
+ fullD?: number;
326
+ }
327
+ /** `cursor.soften`: a preset, a tuning object, `true` (= default) or `false` (= `'off'`). */
328
+ export type CursorSoftenOption = CursorSoftenPreset | CursorSoftenTuning | boolean;
329
+ /** The resolved parameter set. */
330
+ export interface CursorSoftenParams {
331
+ blur: boolean;
332
+ alpha: boolean;
333
+ blurK: number;
334
+ blurMaxPx: number;
335
+ blurMaxFrac: number;
336
+ alphaMin: number;
337
+ fullD: number;
338
+ }
339
+ export const CURSOR_SOFTEN_PRESETS: readonly CursorSoftenPreset[];
340
+ export const CURSOR_SOFTEN_DEFAULTS: Readonly<CursorSoftenParams>;
341
+ export const CURSOR_SOFTEN_KEYS: readonly (keyof CursorSoftenTuning)[];
342
+ /** `cursor.soften` resolved over the defaults; throws on a bad value. */
343
+ export function resolveCursorSoften(opt: unknown, who?: string): CursorSoftenParams;
344
+ /**
345
+ * The runtime's lenient string parse (u_cursor_soften_params_parse), e.g. for URL parameters: a
346
+ * value that does not parse or is out of range keeps its default. `changed`: any knob was set.
347
+ */
348
+ export function parseCursorSoften(
349
+ mode?: string | null,
350
+ blurK?: string | null,
351
+ blurMaxPx?: string | null,
352
+ blurMaxFrac?: string | null,
353
+ alphaMin?: string | null,
354
+ fullD?: string | null,
355
+ ): { params: CursorSoftenParams; changed: boolean };
356
+ /**
357
+ * One frame's softening (u_cursor_draw_soften_sized): blur radius (panel px) = blurK × |ghostPx|,
358
+ * capped at blurMaxPx and blurMaxFrac × `spritePx`; opacity eases 1 → alphaMin by a smoothstep
359
+ * over |d| up to fullD. (0, 1) on the glass.
360
+ */
361
+ export function cursorSoften(d: number, ghostPx: number, spritePx?: number, params?: CursorSoftenParams | null): { blurPx: number; alpha: number };
362
+ /** Largest panel-px distance between the hotspot in NEIGHBOURING views (canvas-normalised origins). */
363
+ export function cursorGhostPx(origins: ArrayLike<ArrayLike<number> | null> | null, pxW: number, pxH: number): number;
364
+ /** Canvas-normalised [u, v] (v down) of a point seen from a view, or null when not in front of it. */
365
+ export function cursorProjectToCanvas(view: CursorView | XRView, point: ArrayLike<number>): [number, number] | null;
366
+ /** A placement's softening; `radius` / `aa` in arrow heights; `soft` false = draw the crisp arrow. */
367
+ export interface CursorSoftening {
368
+ blurPx: number;
369
+ alpha: number;
370
+ ghostPx: number;
371
+ spritePx: number;
372
+ radius: number;
373
+ aa: number;
374
+ soft: boolean;
375
+ }
376
+ /** The softening for a placement drawn with `views` on a `panelW` × `panelH` px canvas, the arrow `heightFraction` of it tall. */
377
+ export function cursorPlacementSoften(
378
+ placement: CursorPlacement | { active: false } | null,
379
+ views: ArrayLike<CursorView | XRView>,
380
+ panelW: number,
381
+ panelH: number,
382
+ heightFraction: number,
383
+ params?: CursorSoftenParams,
384
+ ): CursorSoftening;
385
+ /** The runtime's 13-tap blur kernel, `[dx, dy, weight]` in units of the radius. */
386
+ export const CURSOR_SOFTEN_KERNEL: ReadonlyArray<readonly [number, number, number]>;
387
+ /** The arrow's [width, height] in its own units (height 1). */
388
+ export const CURSOR_ARROW_SIZE: readonly [number, number];
389
+ /** The arrow's box padded by `radius` (+ `aa`), arrow units, y down, tip at the origin: [x0, y0, x1, y1]. */
390
+ export function cursorSoftArrowBox(radius: number, aa?: number): [number, number, number, number];
391
+ /** Signed distance to a polygon (negative inside). */
392
+ export function cursorPolygonSd(poly: ReadonlyArray<readonly [number, number]>, x: number, y: number): number;
393
+ /** The softened arrow at (x, y) of arrow units: straight RGB + alpha (before opacity). The shader's reference. */
394
+ export function cursorSoftArrowSample(
395
+ x: number,
396
+ y: number,
397
+ radius: number,
398
+ aa: number,
399
+ fill?: readonly number[],
400
+ outline?: readonly number[],
401
+ polys?: ReadonlyArray<ReadonlyArray<readonly [number, number]>>,
402
+ ): [number, number, number, number];
403
+ /** GLSL defining `vec4 inline3dSoftArrow(vec2 p, float radius, float aa, vec3 fill, vec3 outline)`. */
404
+ export function cursorSoftArrowGLSL(polys?: ReadonlyArray<ReadonlyArray<readonly [number, number]>>): string;
405
+ /** The softened arrow rasterised for a 2D canvas, its tip at (cx, cy), `sizePx` tall: straight-alpha RGBA bytes. */
406
+ export function cursorSoftArrowRaster(
407
+ cx: number,
408
+ cy: number,
409
+ sizePx: number,
410
+ radius: number,
411
+ alpha: number,
412
+ fill?: readonly number[],
413
+ outline?: readonly number[],
414
+ polys?: ReadonlyArray<ReadonlyArray<readonly [number, number]>>,
415
+ ): { x0: number; y0: number; width: number; height: number; data: Uint8ClampedArray };
416
+
417
+ /** A shader snippet pair for a PlayCanvas `pc.ShaderMaterial`: `declarations` above `main`, `output` inside `#ifdef PICK_PASS`. */
418
+ export interface CursorDepthPickSnippet {
419
+ readonly declarations: string;
420
+ readonly output: string;
421
+ }
422
+ /**
423
+ * PlayCanvas ShaderMaterial opt-in to `cursor: 'depth'`'s pick (GLSL): writes the pick id to
424
+ * `pcFragColor0` and, under `DEPTH_PICK_PASS`, the pick depth to `pcFragColor1` (the engine's `pickPS`).
425
+ */
426
+ export const CURSOR_DEPTH_PICK_GLSL: CursorDepthPickSnippet;
427
+ /** The WGSL twin of CURSOR_DEPTH_PICK_GLSL: writes `output.color` / `output.color1`. */
428
+ export const CURSOR_DEPTH_PICK_WGSL: CursorDepthPickSnippet;