@nakednous/host 0.0.9 → 0.0.11

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/README.md CHANGED
@@ -82,6 +82,7 @@ dependency first and an options object last, and registers with the context so
82
82
  | `host.poseTrack(opts)` · `host.cameraTrack(cam, opts)` | core tracks ticked while they play; `{ handles }` builds `TrackHandles`, one handle per keyframe. `add(depth)` — the transport panel's `+` — authors where the camera is looking: a pose track places the pose at the centre of the installed camera's frustum, `depth` `0`…`1` from the near plane to the far one, aimed along that camera (the view bag's matrices; never deduplicated), a camera track captures its camera state, a placement depth being nothing a camera keyframe could use. `remove()` — the transport panel's `−` — retracts the last keyframe, one per call; `remove(i)` is the core track's own |
83
83
  | `host.hid(opts)` · `host.gamepad(opts)` | rate streams — `lin[3]` / `ang[3]` in raw device units, `bind(helm)` feeds it each tick; `available`, `connected` |
84
84
  | `host.image(url)` · `host.video(opts)` · `host.raster(draw, w, h)` | texture sources — an `ImageBitmap`, a hidden `<video>` from a file or the camera with a `ready` promise, a Canvas2D drawing |
85
+ | `decodeRGBE(bytes, opts)` · `encodeRGBE(r, g, b, out)` | a Radiance `.hdr` file's RGBE bytes decoded to `{ data, width, height }` in linear light — three 8-bit mantissas and a shared exponent, `level · 2^(e − 136)` — and the inverse. A floating-point texture's pixels, which no browser image decoder gives: `loadImage` hands back 8-bit codes clamped at 1.0, so a tone-mapping pass would have nothing left to compress. Rows come back in the file's own order (`rowOrder: 'BOTTOM_UP'` for GL's, which is what `webgl.tree`'s `loadHDR` asks for); `maxPixels` refuses past 16 Mpixel rather than exhaust the tab. Pure, DOM-free, one allocation. Verified against three.js's `HDRLoader` and ImageMagick |
85
86
  | `host.mesh(url, opts)` | one mesh — the arrays shape `{ position, indices, normal?, tangent?, texcoord?, color?, joints?, weights? }` with its `bounds` `{ min, max, center, diag }` beside them — from an OBJ file, a one-part glTF 2.0 file (`.glb`, `.gltf`), or any format through `{ parse: (arrayBuffer) => arrays }`; what a bridge's buffer upload takes as it is, and `size / mesh.bounds.diag` fits any file to `size`. A file of several parts rejects, naming `model`. `{ format }` when the URL does not say. **`normals`**: left out, the file's normals are kept and a mesh without any gets smooth ones; `'smooth'` always recomputes them, summed per position, so a faceted file or a texture seam smooths too; `'flat'` always recomputes them on the flattened mesh (every triangle owning its vertices: more vertices, indices counting up); `false` computes nothing. The procedure, its limits and sources are tree's `mesh` module's; glTF viewers compute flat normals for a file without any — here that is `'flat'` |
86
87
  | `host.model(url, opts)` | a model with its structure — `{ parts: [{ name, node, skin, mesh, targets, color }], nodes: { names, parents, rest }, skins: [{ name, joints, inverseBind }], clips: [{ name, duration, channels }] }` — what a rig, morph targets or animation need. A part's `mesh` is as `host.mesh` returns it, in the part's own space; `targets` are morph deltas; nodes come parents first with a rest pose, and with `skins` and `clips` feed tree's `clipSample` · `poseBlend` · `poseWorld` · `jointPalette` as they are. Same options; `'flat'` flattens each part's morph targets with its mesh. An OBJ file is one white part under one identity node. host's own parsers: no textures, cameras, sparse accessors or compressed geometry |
87
88
  | `host.labels` · `host.hasLabels` | the label layer, created on first access — `set(id, text, x, y, z, opts)` at a world anchor, `setScreen(id, text, sx, sy, opts)` in canvas px, `{ frame: true }` for a label that lives one frame, `tick()`; `hasLabels` probes without creating |
@@ -92,7 +93,8 @@ dependency first and an options object last, and registers with the context so
92
93
  The module-level functions behind the methods are exported too (`createView`, `createPointer`,
93
94
  `createPlayers`, `createLoop`, `observeCanvas`, `Handle`, `PointerRouter`, `cameraHelm`,
94
95
  `poseHelm`, `helmBasis`, `poseTrack`, `cameraTrack`, `TrackHandles`, `createHid`,
95
- `createGamepad`, `decodeSpaceNavigator`, `loadImage`, `createVideo`, `raster`, `loadMesh`, `loadModel`, `createLabels`,
96
+ `createGamepad`, `decodeSpaceNavigator`, `loadImage`, `createVideo`, `raster`, `loadMesh`, `loadModel`,
97
+ `decodeRGBE`, `encodeRGBE`, `createLabels`,
96
98
  `createOrbit`), so an adapter can compose them on its own context.
97
99
 
98
100
  ---
package/dist/index.js CHANGED
@@ -760,7 +760,7 @@ class Handle {
760
760
  c.s = this._savedS;
761
761
  c._dialPoint();
762
762
  } else {
763
- c.seed(this._saved[0], this._saved[1], this._saved[2]);
763
+ c.seed(this._saved[0], this._saved[1], this._saved[2], POINT);
764
764
  }
765
765
  if (this._binder) this._binder.set(this.value(this._bindVal));
766
766
  this._grabbed = false;
@@ -854,7 +854,7 @@ class Handle {
854
854
  _v3[1] = gy > 0 ? Math.round(c.pt[1] / gy) * gy : c.pt[1];
855
855
  _v3[2] = gz > 0 ? Math.round(c.pt[2] / gz) * gz : c.pt[2];
856
856
  if (this._isView) { c.pt[0] = _v3[0]; c.pt[1] = _v3[1]; c.pt[2] = _v3[2]; }
857
- else c.seed(_v3[0], _v3[1], _v3[2]);
857
+ else c.seed(_v3[0], _v3[1], _v3[2], POINT);
858
858
  } else if (c.kind === AXIS || c.kind === DIAL) {
859
859
  const step = Array.isArray(sn) ? sn[0] : sn;
860
860
  if (!(step > 0)) return;
@@ -901,6 +901,11 @@ class Handle {
901
901
  * accessor, or a vec3 array mutated in place (a camera state's eye or
902
902
  * center is one). get() seeds the constraint now; each solve calls
903
903
  * set(value) with the reused value array, then fires onChange. Chainable.
904
+ *
905
+ * The array lives in the handle's `report` space, both ways: a SPHERE handle
906
+ * (DIRECTION by default) binds and receives a **unit heading**, not a point on
907
+ * its sphere; a PLANE / AXIS handle binds and receives a world point. Construct
908
+ * the handle with `report: POINT` to bind a point to a sphere instead.
904
909
  * @param {number[]|{ get:Function, set:Function }} target
905
910
  * @returns {Handle} this
906
911
  */
@@ -920,7 +925,9 @@ class Handle {
920
925
  return this;
921
926
  }
922
927
 
923
- /** Re-seed the constraint from the bound target after it changed externally. Chainable. */
928
+ /** Re-seed the constraint from the bound target after it changed externally —
929
+ * the target holds the handle's own `report` space, as the write-back does, so
930
+ * seed → solve → write never changes what the array means. Chainable. */
924
931
  sync() {
925
932
  if (this._binder) this._seedFromBinding();
926
933
  return this;
@@ -931,7 +938,7 @@ class Handle {
931
938
  if (g == null) return;
932
939
  const x = _vx$1(g, 0, 0), y = _vx$1(g, 1, 0), z = _vx$1(g, 2, 0);
933
940
  if (this._isView) { const pt = this._constraint.pt; pt[0] = x; pt[1] = y; pt[2] = z; }
934
- else this._constraint.seed(x, y, z);
941
+ else this._constraint.seed(x, y, z, this._constraint.report);
935
942
  }
936
943
 
937
944
  // Push the freshly solved value to the binding and fire onChange, once per solve.
@@ -982,7 +989,7 @@ class Handle {
982
989
  c.pt[0] = c.anchor[0] + c.s * c.u[0];
983
990
  c.pt[1] = c.anchor[1] + c.s * c.u[1];
984
991
  c.pt[2] = c.anchor[2] + c.s * c.u[2];
985
- } else if (c.kind === PLANE) c.seed(c.pt[0], c.pt[1], c.pt[2]);
992
+ } else if (c.kind === PLANE) c.seed(c.pt[0], c.pt[1], c.pt[2], POINT);
986
993
  }
987
994
  return this;
988
995
  }
@@ -2868,6 +2875,204 @@ function createOrbit(host, cam, opts) {
2868
2875
  return host.register(orbit);
2869
2876
  }
2870
2877
 
2878
+ /**
2879
+ * @file HDR — Radiance (.hdr) RGBE bytes decoded to the pixels a bridge uploads.
2880
+ * @module host/hdr
2881
+ * @license AGPL-3.0-only
2882
+ *
2883
+ * A floating-point texture needs radiance, and no image decoder in the browser
2884
+ * gives it any: `loadImage` hands back an `ImageBitmap` of 8-bit codes, clamped
2885
+ * at 1.0, so a tone-mapping pass would have nothing left to compress. Radiance's
2886
+ * RGBE is the one high-range format a few dozen lines decode.
2887
+ *
2888
+ * ```
2889
+ * const hdr = decodeRGBE(bytes) // { data, width, height } — radiance
2890
+ * const tex = texture(gl, hdr) // the bridge's upload, RGBA32F
2891
+ * ```
2892
+
2893
+ *
2894
+ * A pixel is three 8-bit mantissas and a shared exponent — `v · 2^(e − 136)` —
2895
+ * the format of Greg Ward's RADIANCE (Ward, *Real Pixels*, Graphics Gems II,
2896
+ * 1991). The reader follows `rgbe.c`'s structure and three.js's `HDRLoader`
2897
+ * (itself adapted from Bruce Walter's code), with the exact scale above rather
2898
+ * than that implementation's `v · 2^(e − 128) / 255`, and no `+ 0.5` — the
2899
+ * offset `rgbe.c`'s floating read carries, which lifts every pixel by half a
2900
+ * mantissa step (0.04 % of the mean on a Poly Haven HDRI) and which no file
2901
+ * declares. Rows keep the order the file states and are never flipped here: GL's
2902
+ * bottom-up space is the upload's concern (see texture.js).
2903
+ *
2904
+ * `loadHDR` — the fetch and the GL row order — is `webgl.tree`'s, and it
2905
+ * re-exports these two; a reader with no bridge to feed uses them directly. Rows
2906
+ * come back in the file's own order unless `rowOrder: 'BOTTOM_UP'` asks for GL's.
2907
+ * The decoder is trusted by round-tripping through the encoder and by agreeing
2908
+ * with three.js's `HDRLoader` and ImageMagick on real Radiance files.
2909
+ */
2910
+
2911
+
2912
+ // Radiance's own threshold: at or below it a float holds no light, so a pixel
2913
+ // that low is written as four zero bytes and read back as black.
2914
+ const _MIN_RADIANCE = 1e-32;
2915
+ // 16 Mpixel of RGBA32F is 256 MB of output: past this the tab is gone before
2916
+ // anything on screen says why, so the refusal comes from here.
2917
+ const _DEFAULT_MAX_PIXELS = 16 * 1024 * 1024;
2918
+
2919
+ const _fv = new DataView(new ArrayBuffer(4));
2920
+
2921
+ // The shared exponent as a float32: the bit pattern of 2^(e − 136) — mantissa
2922
+ // zero, biased exponent — read straight out of the view, no Math.pow per pixel.
2923
+ // 256 entries, made once at load, indexed by the exponent byte a pixel carries.
2924
+ const _SCALES = new Float32Array(256);
2925
+ for (let e = 0; e < 256; e++) {
2926
+ _fv.setUint32(0, e < 10 ? 0 : (e - 9) << 23, true); // e < 10 underflows to zero
2927
+ _SCALES[e] = _fv.getFloat32(0, true);
2928
+ }
2929
+
2930
+ /**
2931
+ * Encode three linear radiance samples and a shared exponent to RGBE bytes.
2932
+ * The inverse of a pixel in `decodeRGBE`: the largest component becomes a byte
2933
+ * level between 128 and 255 — `level = round(v / 2^(e − 136))` with e the
2934
+ * exponent that carries it — so all eight bits are spent and the components
2935
+ * keep their ratio. The level is an integer: 0.0 comes back exactly, and
2936
+ * everything else to within half a level, the 1/512 an 8-bit mantissa costs at
2937
+ * worst.
2938
+ * @param {number} r Red, ≥ 0.
2939
+ * @param {number} g Green.
2940
+ * @param {number} b Blue.
2941
+ * @param {Uint8Array} [out] Four bytes to write; a new one when absent.
2942
+ * @returns {Uint8Array} out
2943
+ */
2944
+ function encodeRGBE(r, g, b, out) {
2945
+ const o = out || new Uint8Array(4);
2946
+ const max = Math.max(r, g, b);
2947
+ if (!(max >= _MIN_RADIANCE)) { o[0] = o[1] = o[2] = o[3] = 0; return o; }
2948
+ // The exponent whose scale carries max as a byte level of 128 to 255 — the
2949
+ // finest step the format has for this magnitude. The float32's own exponent
2950
+ // sets the base, one scale step below the level 255 needs, and the search
2951
+ // steps up until max fits a byte. `_SCALES` is the decoder's own table, so
2952
+ // the two cannot drift apart.
2953
+ _fv.setFloat32(0, max, true);
2954
+ let e = Math.min(255, Math.max(1, ((_fv.getUint32(0, true) >>> 23) & 0xff) - 127 + 128));
2955
+ while (e < 255 && max / _SCALES[e] > 255) e++;
2956
+ o[0] = Math.min(255, Math.round(r / _SCALES[e]));
2957
+ o[1] = Math.min(255, Math.round(g / _SCALES[e]));
2958
+ o[2] = Math.min(255, Math.round(b / _SCALES[e]));
2959
+ o[3] = e;
2960
+ return o;
2961
+ }
2962
+
2963
+ // The header: a magic line, keyed lines and comments, a blank line, then the
2964
+ // resolution string (`-Y 512 +X 1024`). Anything before the resolution is
2965
+ // skipped — XMP and other writers put blocks there — so the walk stops at the
2966
+ // first line that states a dimension.
2967
+ function _headerAt(data) {
2968
+ let pos = 0, height = 0, width = 0, flipRows = false;
2969
+ const n = data.length;
2970
+ while (pos < n) {
2971
+ let end = pos;
2972
+ while (end < n && data[end] !== 10) end++;
2973
+ const line = String.fromCharCode.apply(null, data.subarray(pos, Math.min(end, pos + 400)));
2974
+ pos = end + 1;
2975
+ if (/^#\?/.test(line)) continue;
2976
+ const res = /^\s*([-+])([XY])\s+(\d+)\s+([-+])([XY])\s+(\d+)\s*$/.exec(line);
2977
+ if (res) {
2978
+ const first = res[2].toUpperCase(), second = res[5].toUpperCase();
2979
+ if (first === 'Y' && second === 'X') {
2980
+ height = +res[3]; width = +res[6];
2981
+ flipRows = res[1] === '+'; // +Y: row 0 at the bottom
2982
+ } else if (first === 'X' && second === 'Y') {
2983
+ width = +res[3]; height = +res[6];
2984
+ flipRows = res[1] === '+'; // +X: column 0 on the right
2985
+ } else {
2986
+ throw new Error('[host] hdr: the resolution names ' + first + ' then ' + second + '; -Y h +X w expected.');
2987
+ }
2988
+ return { width, height, flipRows, pos };
2989
+ }
2990
+ if (line === '') continue; // a writer may separate the keys from the resolution
2991
+ }
2992
+ throw new Error('[host] hdr: no resolution line — the file is not a Radiance image.');
2993
+ }
2994
+
2995
+ // One scanline of RGBE bytes into `row`: either the new-style RLE (4 planes,
2996
+ // runs of 128 bytes or fewer) or the flat tuples an old writer leaves.
2997
+ function _scanline(data, pos, row, w, off, rowBytes) {
2998
+ const end = data.length;
2999
+ if (w >= 8 && w < 32768 && pos + 3 < end && data[pos] === 2 && data[pos + 1] === 2 && (data[pos + 2] & 0x80) === 0) {
3000
+ const scanline = (data[pos + 2] << 8) | data[pos + 3];
3001
+ if (scanline !== w) throw new Error('[host] hdr: scanline width ' + scanline + ', image width ' + w + '.');
3002
+ pos += 4;
3003
+ for (let c = 0; c < 4; c++) {
3004
+ let x = 0;
3005
+ while (x < w) {
3006
+ if (pos >= end) throw new Error('[host] hdr: the file ends inside scanline ' + (off / rowBytes) + '.');
3007
+ const count = data[pos++];
3008
+ if (count > 128) { // a run of one byte
3009
+ if (pos >= end) throw new Error('[host] hdr: the file ends inside a run.');
3010
+ const v = data[pos++];
3011
+ for (let i = 0; i < count - 128; i++) row[(x + i) * 4 + c] = v;
3012
+ x += count - 128;
3013
+ } else { // a literal run
3014
+ if (count === 0 || pos + count > end) throw new Error('[host] hdr: bad run length in scanline ' + (off / rowBytes) + '.');
3015
+ for (let i = 0; i < count; i++) row[(x + i) * 4 + c] = data[pos + i];
3016
+ pos += count;
3017
+ x += count;
3018
+ }
3019
+ if (x > w) throw new Error('[host] hdr: a run overruns scanline ' + (off / rowBytes) + '.');
3020
+ }
3021
+ }
3022
+ return pos;
3023
+ }
3024
+ // flat: 4 bytes per pixel, in file order (the scanline is one plane-less array)
3025
+ if (pos + w * 4 > end) throw new Error('[host] hdr: the file ends inside scanline ' + (off / rowBytes) + '.');
3026
+ for (let i = 0; i < w * 4; i++) row[i] = data[pos + i];
3027
+ return pos + w * 4;
3028
+ }
3029
+
3030
+ /**
3031
+ * Decode Radiance RGBE bytes into the pixel source `texture` takes.
3032
+ * @details Every pixel becomes four float channels — R, G, B and 1 — and the
3033
+ * rows come back in the order the file states, top-down for the `-Y` every
3034
+ * writer emits. `loadHDR` is the one that turns them over for GL; a caller
3035
+ * here that wants the same passes `rowOrder: 'BOTTOM_UP'`. The decoder holds no
3036
+ * state, allocates its output once, and needs no DOM, so it runs under
3037
+ * node:test as well as in the browser.
3038
+ * @param {ArrayBuffer|Uint8Array} bytes The whole file.
3039
+ * @param {{ rowOrder?:string, maxPixels?:number }} [opts]
3040
+ * rowOrder: `FILE` (default) or `BOTTOM_UP` — the latter for GL's
3041
+ * texture space, row 0 at v = 0.
3042
+ * maxPixels: refuse past this many pixels (default 16 MPixel).
3043
+ * @returns {{ data:Float32Array, width:number, height:number }} Radiance in
3044
+ * linear light: what `texture` uploads as RGBA32F, and the range a
3045
+ * tone operator compresses.
3046
+ */
3047
+ function decodeRGBE(bytes, opts) {
3048
+ const o = opts || {};
3049
+ const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
3050
+ const head = _headerAt(data);
3051
+ const { width: w, height: h } = head;
3052
+ if (!(w > 0) || !(h > 0)) throw new Error('[host] hdr: the resolution states ' + w + ' × ' + h + '.');
3053
+ const max = o.maxPixels || _DEFAULT_MAX_PIXELS;
3054
+ if (w * h > max) {
3055
+ throw new Error('[host] hdr: ' + w + ' × ' + h + ' is past maxPixels (' + max +
3056
+ '); pass a bigger one, or decode at a lower resolution.');
3057
+ }
3058
+ const flip = o.rowOrder === 'BOTTOM_UP';
3059
+ const out = new Float32Array(w * h * 4);
3060
+ const row = new Uint8Array(w * 4);
3061
+ let pos = head.pos;
3062
+ for (let y = 0; y < h; y++) {
3063
+ pos = _scanline(data, pos, row, w, y * w * 4, w * 4);
3064
+ const at = (flip ? h - 1 - y : y) * w * 4;
3065
+ for (let x = 0; x < w; x++) {
3066
+ const p = x * 4, s = _SCALES[row[p + 3]], d = at + p;
3067
+ out[d] = row[p] * s;
3068
+ out[d + 1] = row[p + 1] * s;
3069
+ out[d + 2] = row[p + 2] * s;
3070
+ out[d + 3] = 1;
3071
+ }
3072
+ }
3073
+ return { data: out, width: w, height: h };
3074
+ }
3075
+
2871
3076
  /**
2872
3077
  * @file The host context — one per canvas: pointer, view bag, players, loop, observer.
2873
3078
  * @module host
@@ -3039,5 +3244,5 @@ function createHost(canvas, opts) {
3039
3244
  return host;
3040
3245
  }
3041
3246
 
3042
- export { GAMEPAD_MAP, HID_FILTERS, Handle, PointerRouter, TrackHandles, VIEW, cameraHelm, cameraTrack, createGamepad, createHid, createHost, createLabels, createLoop, createOrbit, createPlayers, createPointer, createVideo, createView, decodeSpaceNavigator, helmBasis, isConstraint, loadImage, loadMesh, loadModel, measureCanvas, observeCanvas, poseHelm, poseTrack, raster, validConstraint };
3247
+ export { GAMEPAD_MAP, HID_FILTERS, Handle, PointerRouter, TrackHandles, VIEW, cameraHelm, cameraTrack, createGamepad, createHid, createHost, createLabels, createLoop, createOrbit, createPlayers, createPointer, createVideo, createView, decodeRGBE, decodeSpaceNavigator, encodeRGBE, helmBasis, isConstraint, loadImage, loadMesh, loadModel, measureCanvas, observeCanvas, poseHelm, poseTrack, raster, validConstraint };
3043
3248
  //# sourceMappingURL=index.js.map