@displayxr/inline3d 1.40.1 → 1.41.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.
@@ -7,37 +7,45 @@
7
7
  // two halves ARE the depth: this is ADR-046 Phase 3b done in the page. Block-match a cursor-sized
8
8
  // footprint between the eyes and take the nearest confident match.
9
9
  //
10
- // The cursor only ever comes FORWARD (David, 2026-10-10): it never goes inside the display. While
11
- // what is under it sits at or behind the glass, it is simply the OS arrow, on the screen plane.
12
- // Only when content comes out of the screen under it is the arrow drawn into both halves, with that
13
- // content's disparity plus a small margin (the OS cursor hidden meanwhile), so it is never overlapped
14
- // by what pops out. It is the normal arrow either way, not a special sprite.
15
- //
16
- // Same filter as the scene cursor (./inline3d-cursor-depth.js): fast rise, slow sink, the plane when
17
- // nothing matches. Disparities here are in OUTPUT pixels of one eye of the woven buffer, + = crossed
18
- // (in front of the glass), the sign the rest of camera/ uses. Loaded only when a page asks for
10
+ // The policy is the one every DisplayXR depth cursor follows (runtime ADR-046 Amendment 2), taken
11
+ // from the shared core (../inline3d-cursor-depth.js), not re-implemented here:
12
+ // - never behind the glass: only content that comes OUT of the screen moves it, to the margin
13
+ // (0.03 eye baselines, ~2 mm) in front of that content (cursorTarget);
14
+ // - on the plane it IS the OS cursor: the arrow is drawn into both halves only while the filtered
15
+ // disparity is past the draw threshold (-0.005 baselines, and >= 0.5 px; cursorShouldDraw), the
16
+ // OS cursor hidden exactly then;
17
+ // - the same filter (fast rise, slow sink) and the same arrow (CURSOR_ARROW, tip = hotspot, the
18
+ // OS arrow's 19 CSS px).
19
+ // What is specific to a pair: it has no 3D views, so it works in tile PIXEL disparity — OUTPUT
20
+ // pixels of one eye of the woven buffer, + = crossed (in front of the glass), the sign the rest of
21
+ // camera/ uses — and converts to the core's eye-baseline units through the panel's pixel pitch:
22
+ // buffer px per metre = outW / tileCssWidth / metersPerCssPx (the tile handle's displayMetrics()),
23
+ // times the eye separation (displayMetrics' default, 63 mm). Loaded only when a page asks for
19
24
  // `cursor: 'depth'` — zero cost otherwise.
20
25
 
21
- import { CursorPointer, cursorFootprint, cursorFilterStep, CURSOR_DEFAULT_TUNING } from '../inline3d-cursor-depth.js';
26
+ import {
27
+ CursorPointer,
28
+ cursorArrowFootprint,
29
+ cursorArrowInset,
30
+ cursorDefaultHeight,
31
+ cursorFilterStep,
32
+ cursorShouldDraw,
33
+ cursorTarget,
34
+ CURSOR_ARROW,
35
+ CURSOR_ARROW_CSS_PX,
36
+ CURSOR_DEFAULT_TUNING,
37
+ } from '../inline3d-cursor-depth.js';
38
+ import { DISPLAY_METRICS_DEFAULTS } from '../inline3d-display-metrics.js';
22
39
  import { matchBlock, MIN_NCC } from './disparity.js';
23
40
 
24
- /** The arrow's height in CSS px — the OS arrow's apparent size, so the swap to the drawn one is seamless. */
25
- export const PAIR_CURSOR_CSS_PX = 19;
41
+ /** The arrow's height in CSS px — the OS arrow's apparent size (the core's CURSOR_ARROW_CSS_PX). */
42
+ export const PAIR_CURSOR_CSS_PX = CURSOR_ARROW_CSS_PX;
26
43
  /** Arrow height as a fraction of the tile height when the tile's on-page size is unknown. */
27
44
  export const PAIR_CURSOR_HEIGHT = 0.06;
28
- /** Below this many output px in front of the glass the drawn arrow hands back to the OS cursor. */
29
- export const PAIR_CURSOR_SHOW_PX = 0.5;
30
- /**
31
- * The standard pointer arrow, hotspot at the tip (0, 0), unit = its height, x right, y down: the
32
- * OS arrow's outline (a 19-px-tall Windows/macOS arrow, normalised).
33
- */
34
- export const PAIR_CURSOR_ARROW = Object.freeze([
35
- [0, 0], [0, 16 / 19], [4.2 / 19, 12.4 / 19], [6.9 / 19, 18.6 / 19], [9.5 / 19, 17.5 / 19], [6.8 / 19, 11.5 / 19], [11.7 / 19, 11.5 / 19],
36
- ]);
37
- /** How far in front of out-of-screen content the arrow floats, as a fraction of the eye width (crossed). */
38
- export const PAIR_CURSOR_MARGIN = 0.002;
39
- /** Comfort cap on how far in front the arrow comes, as a fraction of the eye width. */
40
- export const PAIR_CURSOR_CLAMP = 0.08;
45
+ /** The arrow every DisplayXR depth cursor draws (the core's CURSOR_ARROW): tip at (0, 0), unit = its height, y down. */
46
+ export const PAIR_CURSOR_ARROW = CURSOR_ARROW;
47
+ /** One CSS px in metres when the panel cannot say (the CSS reference pixel, 1/96 in). */
48
+ export const PAIR_CURSOR_CSS_PX_M = 0.0254 / 96;
41
49
 
42
50
  /**
43
51
  * The displayed disparity of the nearest content under the cursor's footprint, or null when no
@@ -65,8 +73,8 @@ export function pairFootprintDisparity(luma, geo, u, v, height, aspect) {
65
73
  const { rL, rR } = geo;
66
74
  const k = geo.outW / rL.sw; // output px per source px
67
75
  let best = null;
68
- // The ring is centred on the arrow's body, not its tip: that is what content must not overlap.
69
- for (const [pu, pv] of cursorFootprint(u + (0.3 * height) / aspect, v + 0.5 * height, height, aspect)) {
76
+ // The tip, then a ring centred on the arrow's body: that is what content must not overlap.
77
+ for (const [pu, pv] of cursorArrowFootprint(u, v, height, aspect)) {
70
78
  if (!(pu >= 0 && pu <= 1 && pv >= 0 && pv <= 1)) continue;
71
79
  const su = geo.mirror ? 1 - pu : pu;
72
80
  const x = Math.round((rL.sx + su * rL.sw) * scale - b / 2);
@@ -82,38 +90,68 @@ export function pairFootprintDisparity(luma, geo, u, v, height, aspect) {
82
90
  }
83
91
 
84
92
  /**
85
- * The arrow's target disparity: in front of content that comes OUT of the screen (its disparity
86
- * plus the margin, clamped), else the glass. Never behind the glass — the cursor does not go inside
87
- * the display.
93
+ * Output px of one eye per metre on the glass: the buffer eye is stretched over the tile's CSS
94
+ * width, and one CSS px is `metersPerCssPx` on the panel (the CSS reference px when unknown).
95
+ */
96
+ export function pairCursorScale(outW, tileCssWidth, metersPerCssPx) {
97
+ const m = metersPerCssPx > 0 && Number.isFinite(metersPerCssPx) ? metersPerCssPx : PAIR_CURSOR_CSS_PX_M;
98
+ return tileCssWidth > 0 ? outW / tileCssWidth / m : outW / (1000 * m);
99
+ }
100
+
101
+ /**
102
+ * Output px of on-screen disparity per eye-baseline unit — the conversion between this module's
103
+ * pixels and the core's units: pairCursorScale × the eye separation.
88
104
  */
89
- export function pairCursorTarget(contentPx, outW) {
90
- if (contentPx === null || !Number.isFinite(contentPx) || !(contentPx > 0)) return 0;
91
- return Math.min(PAIR_CURSOR_CLAMP * outW, contentPx + PAIR_CURSOR_MARGIN * outW);
105
+ export function pairPxPerBaseline(outW, tileCssWidth, metersPerCssPx, eyeSeparationM = DISPLAY_METRICS_DEFAULTS.eyeSeparationM) {
106
+ return pairCursorScale(outW, tileCssWidth, metersPerCssPx) * eyeSeparationM;
107
+ }
108
+
109
+ /**
110
+ * The arrow's target disparity in output px (+ = in front): the core's policy (cursorTarget) on
111
+ * the content's disparity, converted through `pxPerBaseline`. In front of content that comes OUT
112
+ * of the screen by the margin (0.03 baselines), else the glass — never behind it.
113
+ */
114
+ export function pairCursorTarget(contentPx, pxPerBaseline, tuning = CURSOR_DEFAULT_TUNING) {
115
+ if (contentPx === null || !Number.isFinite(contentPx) || !(pxPerBaseline > 0)) return 0;
116
+ return -cursorTarget(tuning, true, -contentPx / pxPerBaseline) * pxPerBaseline || 0;
117
+ }
118
+
119
+ /** The arrow's fill, inset by one output px (at least 1/19 of its height), cached per size. */
120
+ const _fillBySize = new Map();
121
+ function arrowFill(sizePx) {
122
+ const d = sizePx > 0 ? Math.max(1 / 19, 1 / sizePx) : 1 / 19;
123
+ const key = Math.round(d * 1e4);
124
+ let f = _fillBySize.get(key);
125
+ if (!f) {
126
+ if (_fillBySize.size > 32) _fillBySize.clear();
127
+ _fillBySize.set(key, (f = cursorArrowInset(d)));
128
+ }
129
+ return f;
92
130
  }
93
131
 
94
132
  /**
95
133
  * Draw the arrow into both halves of a woven buffer: the left eye's copy `disparityPx / 2` right of
96
- * the pointer, the right eye's as far left (crossed = in front). White with a dark outline, like the
97
- * OS arrow. Each copy is clipped to its own half, so an arrow at the edge never bleeds into the other eye.
134
+ * the pointer, the right eye's as far left (crossed = in front). The shared arrow: its dark outline
135
+ * polygon, then the white fill inset by a px; the tip on the hotspot. Each copy is clipped to its
136
+ * own half, so an arrow at the edge never bleeds into the other eye.
98
137
  * @param {number} sizePx arrow height in output px
99
138
  */
100
139
  export function drawPairArrow(g, outW, outH, u, v, disparityPx, sizePx) {
101
140
  const cy = v * outH;
141
+ const fill = arrowFill(sizePx);
102
142
  for (const eye of [0, 1]) {
103
143
  const cx = eye * outW + u * outW + (eye === 0 ? disparityPx / 2 : -disparityPx / 2);
104
144
  g.save();
105
145
  g.beginPath();
106
146
  g.rect(eye * outW, 0, outW, outH);
107
147
  g.clip();
108
- g.beginPath();
109
- PAIR_CURSOR_ARROW.forEach(([x, y], k) => (k ? g.lineTo : g.moveTo).call(g, cx + x * sizePx, cy + y * sizePx));
110
- g.closePath();
111
- g.fillStyle = '#fff';
112
- g.fill();
113
- g.lineWidth = Math.max(1, sizePx / 19);
114
- g.lineJoin = 'miter';
115
- g.strokeStyle = '#000';
116
- g.stroke();
148
+ for (const [poly, style] of [[CURSOR_ARROW, '#000'], [fill, '#fff']]) {
149
+ g.beginPath();
150
+ poly.forEach(([x, y], k) => (k ? g.lineTo : g.moveTo).call(g, cx + x * sizePx, cy + y * sizePx));
151
+ g.closePath();
152
+ g.fillStyle = style;
153
+ g.fill();
154
+ }
117
155
  g.restore();
118
156
  }
119
157
  }
@@ -133,7 +171,12 @@ export class PairCursor {
133
171
  this._luma = null;
134
172
  this._uv = null;
135
173
  this._content = null;
136
- /** The last frame's placement, for diagnostics: `{ contentPx, targetPx, disparityPx, drawn }` or null. */
174
+ this._metricsFor = null; // the tile handle whose displayMetrics() was asked for
175
+ /** One CSS px on the panel, metres, once the tile's handle has said (else the CSS reference px). */
176
+ this.metersPerCssPx = null;
177
+ /** The eye separation (metres) the baseline units convert with: displayMetrics' (a default today). */
178
+ this.eyeSeparationM = DISPLAY_METRICS_DEFAULTS.eyeSeparationM;
179
+ /** The last frame's placement, for diagnostics: `{ contentPx, targetPx, disparityPx, filteredPx, drawn, pxPerBaseline }` (output px, + = in front) or null. */
137
180
  this.last = null;
138
181
  }
139
182
 
@@ -146,7 +189,8 @@ export class PairCursor {
146
189
  * exactly then); otherwise leaves the buffer alone and the OS arrow shows, on the plane.
147
190
  * @param {CanvasRenderingContext2D} g the woven buffer (2·outW × outH), already painted
148
191
  * @param {object|null} luma the latest grayscale source copy (pairFootprintDisparity)
149
- * @param {{rL: object, rR: object, outW: number, outH: number, mirror?: boolean}} geo
192
+ * @param {{rL: object, rR: object, outW: number, outH: number, mirror?: boolean, handle?: object}} geo
193
+ * `handle` = the tile's woven handle: its `displayMetrics()` gives the panel's pixel pitch
150
194
  * @param {number} nowSec
151
195
  */
152
196
  draw(g, luma, geo, nowSec) {
@@ -160,20 +204,39 @@ export class PairCursor {
160
204
  this._luma = luma;
161
205
  this._uv = uv;
162
206
  }
163
- const target = pairCursorTarget(this._content, geo.outW);
164
- // The shared filter rises fast toward SMALLER values (nearer, in its convention): negate.
165
- const disparity = -cursorFilterStep(this.filter, CURSOR_DEFAULT_TUNING, -target, nowSec);
166
- const drawn = disparity >= PAIR_CURSOR_SHOW_PX;
207
+ this._askMetrics(geo.handle);
208
+ const r = this.pointer.canvas.getBoundingClientRect?.();
209
+ const pxPerBaseline = pairPxPerBaseline(geo.outW, r && r.width, this.metersPerCssPx, this.eyeSeparationM);
210
+ // The core's policy and filter, in its units (eye baselines, < 0 = in front).
211
+ const content = this._content === null ? null : -this._content / pxPerBaseline;
212
+ const target = cursorTarget(CURSOR_DEFAULT_TUNING, content !== null, content);
213
+ const d = cursorFilterStep(this.filter, CURSOR_DEFAULT_TUNING, target, nowSec);
214
+ const drawn = cursorShouldDraw(d, pxPerBaseline);
215
+ const disparity = -d * pxPerBaseline; // output px, + = in front
167
216
  if (drawn) drawPairArrow(g, geo.outW, geo.outH, u, v, disparity, hf * geo.outH);
168
217
  this.pointer.hideCss(drawn); // the OS arrow IS the cursor on the plane
169
- this.last = { contentPx: this._content, targetPx: target, disparityPx: drawn ? disparity : 0, drawn };
218
+ this.last = { contentPx: this._content, targetPx: -target * pxPerBaseline || 0, disparityPx: drawn ? disparity : 0, filteredPx: disparity || 0, drawn, pxPerBaseline };
170
219
  return this.last;
171
220
  }
172
221
 
173
- /** The OS arrow's height as a fraction of the tile's on-page height. */
222
+ /** Ask the tile's handle once for the panel's pixel pitch; until it answers, the CSS reference px. */
223
+ _askMetrics(handle) {
224
+ if (!handle || handle === this._metricsFor || typeof handle.displayMetrics !== 'function') return;
225
+ this._metricsFor = handle;
226
+ Promise.resolve()
227
+ .then(() => handle.displayMetrics())
228
+ .then((m) => {
229
+ if (this._metricsFor !== handle || !m) return;
230
+ if (m.metersPerCssPx > 0) this.metersPerCssPx = m.metersPerCssPx;
231
+ if (m.eyeSeparationM > 0) this.eyeSeparationM = m.eyeSeparationM;
232
+ })
233
+ .catch(() => {});
234
+ }
235
+
236
+ /** The OS arrow's height as a fraction of the tile's on-page height (the core's default size). */
174
237
  _osArrowFraction() {
175
238
  const r = this.pointer.canvas.getBoundingClientRect?.();
176
- return r && r.height > 0 ? Math.min(0.5, PAIR_CURSOR_CSS_PX / r.height) : PAIR_CURSOR_HEIGHT;
239
+ return r && r.height > 0 ? cursorDefaultHeight(r.height) : PAIR_CURSOR_HEIGHT;
177
240
  }
178
241
 
179
242
  /** Not drawing this frame (pointer away, tile not woven): the CSS cursor comes back. */
@@ -1715,7 +1715,7 @@ class Tile {
1715
1715
  const r = (rects[eye] = wire.eyeCropRect(eyeW, H, A, shift, eye));
1716
1716
  g.drawImage(v, eye * eyeW + r.sx, r.sy, r.sw, r.sh, eye * outW, 0, outW, outH);
1717
1717
  }
1718
- if (this.cursor) this.cursor.draw(g, this.luma, { rL: rects[0], rR: rects[1], outW, outH }, performance.now() / 1000);
1718
+ if (this.cursor) this.cursor.draw(g, this.luma, { rL: rects[0], rR: rects[1], outW, outH, handle: this.handle }, performance.now() / 1000);
1719
1719
  }
1720
1720
 
1721
1721
  /**
@@ -762,7 +762,7 @@ class CameraView {
762
762
  }
763
763
  }
764
764
  if (this.cursor) {
765
- const geo = { rL: eyeCropRect(eyeW, H, A, shift, 0), rR: eyeCropRect(eyeW, H, A, shift, 1), outW, outH, mirror: this.mirror };
765
+ const geo = { rL: eyeCropRect(eyeW, H, A, shift, 0), rR: eyeCropRect(eyeW, H, A, shift, 1), outW, outH, mirror: this.mirror, handle: this.handle };
766
766
  this.cursor.draw(g, this._luma, geo, performance.now() / 1000);
767
767
  }
768
768
  }
@@ -14,18 +14,20 @@
14
14
  // runtime's one-frame-late app hint.
15
15
  //
16
16
  // Placement: the same maths as everywhere else (./inline3d-cursor-depth.js, a port of the
17
- // runtime's u_cursor_depth, ADR-046), solved in WORLD space from this frame's views carried
18
- // through the rig node. Drawn as a filled mesh (strokes + a dark outline) in the UI layer with
19
- // depth test off, so every eye camera draws it last and nothing occludes it; the picker skips the
20
- // UI layer, so the cursor never picks itself.
17
+ // runtime's u_cursor_depth, ADR-046 + Amendment 2's policy), solved in WORLD space from this
18
+ // frame's views carried through the rig node. Drawn — only while content under it comes out of the
19
+ // glass — as the classic arrow (the shared cursorArrowMesh: dark outline, white fill, tip on the
20
+ // hotspot) in the UI layer with depth test off, so every eye camera draws it last and nothing
21
+ // occludes it; the picker skips the UI layer, so the cursor never picks itself. On the plane
22
+ // nothing is drawn and the CSS cursor is the cursor.
21
23
 
22
24
  import {
23
25
  CursorDepthPlacer,
24
26
  CursorPointer,
25
- CURSOR_DEFAULT_HEIGHT,
26
27
  CURSOR_DEFAULT_TUNING,
27
- cursorCrosshairMesh,
28
- cursorFootprint,
28
+ cursorArrowMesh,
29
+ cursorArrowFootprint,
30
+ cursorDefaultHeight,
29
31
  cursorViewRay,
30
32
  } from './inline3d-cursor-depth.js';
31
33
 
@@ -42,10 +44,11 @@ export class PlayCanvasDepthCursor {
42
44
  * @param {object} viewer the PlayCanvasSplatViewer (app, rigNode, eye are read lazily).
43
45
  * @param {{canvas: HTMLElement, height?: number, margin?: number, anchor?: string, pointerScope?: string}} opts
44
46
  */
45
- constructor(pc, viewer, { canvas, height = CURSOR_DEFAULT_HEIGHT, margin, anchor = 'hybrid', pointerScope = 'canvas' }) {
47
+ constructor(pc, viewer, { canvas, height, margin, anchor = 'hybrid', pointerScope = 'canvas' }) {
46
48
  this.pc = pc;
47
49
  this.viewer = viewer;
48
- this.height = height > 0 && Number.isFinite(height) ? height : CURSOR_DEFAULT_HEIGHT;
50
+ /** Sprite height (canvas heights) when set; null = the OS arrow's size, from the canvas's on-page height. */
51
+ this.height = height > 0 && Number.isFinite(height) ? height : null;
49
52
  this.pointer = new CursorPointer(canvas, { scope: pointerScope });
50
53
  const tuning = margin > 0 && Number.isFinite(margin) ? { ...CURSOR_DEFAULT_TUNING, margin } : CURSOR_DEFAULT_TUNING;
51
54
  this.placer = new CursorDepthPlacer(tuning, { anchor });
@@ -89,16 +92,17 @@ export class PlayCanvasDepthCursor {
89
92
  console.warn('[inline3d] cursor: depth: a PlayCanvas pick did not answer in time; retrying (cursor on the glass meanwhile).');
90
93
  }
91
94
  }
92
- if (this._frame++ % PICK_EVERY === 0 && !this._inFlight) this._pick(app, rig, entries[0], uv);
95
+ const h = this.height ?? cursorDefaultHeight(this.pointer.cssHeight());
96
+ if (this._frame++ % PICK_EVERY === 0 && !this._inFlight) this._pick(app, rig, entries[0], uv, h);
93
97
  const now = (globalThis.performance ? globalThis.performance.now() : Date.now()) / 1000;
94
- const p = this.placer.update(views, { u: uv[0], v: uv[1], nearestPoint: this.hit, cursorHeight: this.height }, now);
98
+ const p = this.placer.update(views, { u: uv[0], v: uv[1], nearestPoint: this.hit, cursorHeight: h, canvasPx: this.pointer.panelPx() }, now);
95
99
  this.placement = p;
96
100
  this.pointer.hideCss(!!p.active);
97
101
  const sprite = this._sprite || (p.active ? this._makeSprite(app) : null);
98
102
  if (!sprite) return;
99
103
  sprite.mi.visible = !!p.active;
100
104
  if (!p.active) return;
101
- // The unit crosshair in the sprite's XY plane: basis (rotation), height (scale), position.
105
+ // The unit arrow in the sprite's XY plane, tip at its origin: basis (rotation), height (scale), position = the hotspot.
102
106
  const { x, y, z } = p.basis;
103
107
  const q = quatFromMatrix([x[0], x[1], x[2], 0, y[0], y[1], y[2], 0, z[0], z[1], z[2], 0, 0, 0, 0, 1]);
104
108
  sprite.node.setLocalPosition(p.position[0], p.position[1], p.position[2]);
@@ -114,11 +118,11 @@ export class PlayCanvasDepthCursor {
114
118
  this.pointer.hideCss(false);
115
119
  }
116
120
 
117
- // The filled crosshair: one mesh (outline triangles first, fill after — the order a
118
- // depth-test-off draw needs), vertex-coloured, alpha-blended, in the UI layer.
121
+ // The arrow: one mesh (outline triangles first, fill after — the order a depth-test-off draw
122
+ // needs), vertex-coloured, alpha-blended, in the UI layer.
119
123
  _makeSprite(app) {
120
124
  const pc = this.pc;
121
- const m = cursorCrosshairMesh();
125
+ const m = cursorArrowMesh();
122
126
  const mesh = new pc.Mesh(app.graphicsDevice);
123
127
  mesh.setPositions(m.positions);
124
128
  mesh.setColors(m.colors);
@@ -179,7 +183,7 @@ export class PlayCanvasDepthCursor {
179
183
  }
180
184
 
181
185
  // Render the pick buffer from the first eye and read the footprint's world points.
182
- _pick(app, rig, e, uv) {
186
+ _pick(app, rig, e, uv, height) {
183
187
  const pc = this.pc;
184
188
  const aspect = e.width > 0 && e.height > 0 ? e.width / e.height : 1;
185
189
  const W = PICK_WIDTH;
@@ -240,7 +244,7 @@ export class PlayCanvasDepthCursor {
240
244
  const Mw = view.transformMatrix;
241
245
  const f = [-Mw[8], -Mw[9], -Mw[10]];
242
246
  const zNear = cam.camera.nearClip, zFar = cam.camera.farClip;
243
- const samples = cursorFootprint(uv[0], uv[1], this.height, this.pointer.aspect())
247
+ const samples = cursorArrowFootprint(uv[0], uv[1], height, this.pointer.aspect())
244
248
  .filter(([u, v]) => u >= 0 && u <= 1 && v >= 0 && v <= 1)
245
249
  .map(([u, v]) =>
246
250
  picker.getPointDepthAsync(Math.min(W - 1, Math.floor(u * W)), Math.min(H - 1, Math.floor(v * H))).then((lin) => {