@nakednous/host 0.0.5 → 0.0.6

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,7 +82,8 @@ 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 |
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
- | `host.model(url)` | an OBJ model as arrays — `{ position, normal?, texcoord?, indices }`, what twgl's `createBufferInfoFromArrays` takes; host's own parser (fan-triangulated faces, one vertex per distinct position / uv / normal triple), materials ignored |
85
+ | `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
+ | `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 |
86
87
  | `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 |
87
88
  | `host.orbit(cam, opts)` | the fall-through gesture — one pointer orbits, two pan and dolly, the wheel dollies; `damping` in seconds (default 0.2; 0 exact) makes the camera trail the gesture and complete it after release; `update(dt)` reports whether the camera moved; the vertical sense follows the bag's projection (y-up GL, y-flipped p5) |
88
89
  | `host.tick(dt)` | external-loop mode: tick the players; the caller flushes the pointer once its consumers ran |
@@ -91,7 +92,7 @@ dependency first and an options object last, and registers with the context so
91
92
  The module-level functions behind the methods are exported too (`createView`, `createPointer`,
92
93
  `createPlayers`, `createLoop`, `observeCanvas`, `Handle`, `PointerRouter`, `cameraHelm`,
93
94
  `poseHelm`, `helmBasis`, `poseTrack`, `cameraTrack`, `TrackHandles`, `createHid`,
94
- `createGamepad`, `decodeSpaceNavigator`, `loadImage`, `createVideo`, `raster`, `loadModel`, `createLabels`,
95
+ `createGamepad`, `decodeSpaceNavigator`, `loadImage`, `createVideo`, `raster`, `loadMesh`, `loadModel`, `createLabels`,
95
96
  `createOrbit`), so an adapter can compose them on its own context.
96
97
 
97
98
  ---
@@ -154,6 +155,15 @@ npm run build # rollup → dist/index.js
154
155
 
155
156
  ---
156
157
 
158
+ ## Acknowledgements
159
+
160
+ - [glTF 2.0](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html) (Khronos Group) — the format `host/gltf` reads: the GLB container, accessors and buffer views, skins, morph targets, animations.
161
+ - Wavefront OBJ — the format `host/obj` reads.
162
+ - [three.js](https://threejs.org/) — the orbit's damping follows the shape of its `OrbitControls`, made time-based.
163
+ - [twgl](https://twgljs.org/) — the arrays shape a model's meshes arrive in.
164
+
165
+ Both parsers are host's own; no third-party code ships in the package.
166
+
157
167
  ## License
158
168
 
159
169
  AGPL-3.0-only
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { WEBGL, cameraView, cameraProj, mat4Mul, mat4Invert, PLANE, createConstraint, WORLD, AXIS, DIAL, POINT, mapDirection, pixelRatio, rayHitSphere, unproject, SPHERE, dirFromAzEl, DIRECTION, mapLocation, MATRIX, PoseHelm, cameraToPose, EYE, SELF, qToMat4, cameraEye, cameraFromPose, CameraTrack, PoseTrack, qFromAxisAngle, SCREEN, cameraCopy, createCamera, coastAlive, coastStep, cameraOrbit, cameraPan, cameraDolly } from '@nakednous/tree';
1
+ import { WEBGL, cameraView, cameraProj, mat4Mul, mat4Invert, PLANE, createConstraint, WORLD, AXIS, DIAL, POINT, mapDirection, pixelRatio, rayHitSphere, unproject, SPHERE, dirFromAzEl, DIRECTION, mapLocation, MATRIX, PoseHelm, cameraToPose, EYE, SELF, qToMat4, cameraEye, cameraFromPose, CameraTrack, PoseTrack, qFromAxisAngle, mat4ToTransform, meshBounds, meshFlatten, meshNormals, meshGroups, SCREEN, cameraCopy, createCamera, coastAlive, coastStep, cameraOrbit, cameraPan, cameraDolly } from '@nakednous/tree';
2
2
 
3
3
  /**
4
4
  * @file The view bag — the matrices every pick, solve, label and orbit reads.
@@ -1923,12 +1923,203 @@ function parseObj(text) {
1923
1923
  }
1924
1924
 
1925
1925
  /**
1926
- * @file Media sources — images, video and Canvas2D rasters for a bridge's textures, OBJ models for its buffers.
1926
+ * @file glTF 2.0 → meshes in the arrays shape, the node hierarchy, skins and animation clips.
1927
+ * @module host/gltf
1928
+ * @license AGPL-3.0-only
1929
+ *
1930
+ * The format is glTF 2.0 (Khronos Group): a JSON document plus binary buffers,
1931
+ * or both in one GLB container. parseGlb splits a container; parseGltf reads
1932
+ * the document over its buffers into what a bridge uploads and what tree's
1933
+ * skin functions (clipSample, poseWorld, jointPalette) take as they are.
1934
+ *
1935
+ * Read: triangle primitives (POSITION, NORMAL, TANGENT, TEXCOORD_0, JOINTS_0,
1936
+ * WEIGHTS_0, indices), morph targets (POSITION and NORMAL deltas), the
1937
+ * material's base colour factor, nodes, skins, animations. Normalised integer
1938
+ * accessors arrive as floats; interleaved views are unpacked. Not read:
1939
+ * textures, cameras, other primitive modes, sparse accessors and compressed
1940
+ * geometry (Draco, meshopt) — the last two throw.
1941
+ *
1942
+ * Nodes are re-indexed parents first, and every node index in the result
1943
+ * (a mesh's, a skin's joints, a channel's) is the new one.
1944
+ */
1945
+
1946
+
1947
+ const GLB_MAGIC = 0x46546C67, CHUNK_JSON = 0x4E4F534A, CHUNK_BIN = 0x004E4942;
1948
+
1949
+ const COMPONENTS = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
1950
+ const TYPES = {
1951
+ 5120: Int8Array, 5121: Uint8Array, 5122: Int16Array, 5123: Uint16Array, 5125: Uint32Array, 5126: Float32Array,
1952
+ };
1953
+ const NORMALISED = { 5120: 127, 5121: 255, 5122: 32767, 5123: 65535 };
1954
+ const ATTRIBUTES = {
1955
+ POSITION: 'position', NORMAL: 'normal', TANGENT: 'tangent', TEXCOORD_0: 'texcoord',
1956
+ JOINTS_0: 'joints', WEIGHTS_0: 'weights',
1957
+ };
1958
+ const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
1959
+
1960
+ /**
1961
+ * Split a GLB container into its JSON document and binary chunk.
1962
+ * @param {ArrayBuffer} buffer
1963
+ * @returns {{ json:object, bin:ArrayBuffer|null }}
1964
+ */
1965
+ function parseGlb(buffer) {
1966
+ const view = new DataView(buffer);
1967
+ if (view.getUint32(0, true) !== GLB_MAGIC) throw new Error('[host] gltf: not a GLB container.');
1968
+ if (view.getUint32(4, true) !== 2) throw new Error('[host] gltf: GLB version ' + view.getUint32(4, true) + ', 2 expected.');
1969
+ const end = Math.min(view.getUint32(8, true), buffer.byteLength);
1970
+ let json = null, bin = null;
1971
+ for (let at = 12; at + 8 <= end;) {
1972
+ const length = view.getUint32(at, true), type = view.getUint32(at + 4, true);
1973
+ const start = at + 8;
1974
+ if (type === CHUNK_JSON) json = JSON.parse(new TextDecoder().decode(new Uint8Array(buffer, start, length)));
1975
+ else if (type === CHUNK_BIN && !bin) bin = buffer.slice(start, start + length);
1976
+ at = start + length + (-length & 3);
1977
+ }
1978
+ if (!json) throw new Error('[host] gltf: GLB without a JSON chunk.');
1979
+ return { json, bin };
1980
+ }
1981
+
1982
+ /**
1983
+ * Read a glTF document over its buffers.
1984
+ *
1985
+ * A part is one triangle primitive under one node: `mesh` in the
1986
+ * arrays shape (position, normal?, tangent?, texcoord?, joints?, weights?,
1987
+ * indices), `targets` its morph targets as delta arrays, `color` the
1988
+ * material's base colour factor, `node` the node it hangs from and `skin` its
1989
+ * skin's index or −1. A rigid mesh draws under its node's world matrix; a
1990
+ * skinned one under its skin's joint palette.
1991
+ *
1992
+ * @param {object} json The glTF document.
1993
+ * @param {ArrayBuffer[]} buffers One per `json.buffers` entry.
1994
+ * @returns {{ parts:{ name:string, node:number, skin:number, mesh:object,
1995
+ * targets:{ name:string, position?:object, normal?:object }[], color:number[] }[],
1996
+ * nodes:{ names:string[], parents:Int32Array, rest:Float32Array },
1997
+ * skins:{ name:string, joints:Uint16Array, inverseBind:Float32Array }[],
1998
+ * clips:{ name:string, duration:number, channels:object[] }[] }}
1999
+ * nodes.rest: the rest pose, ten numbers per node (translation, rotation
2000
+ * [x,y,z,w], scale). A clip channel: { node, path, interp, times, values }.
2001
+ */
2002
+ function parseGltf(json, buffers) {
2003
+ for (const ext of json.extensionsRequired || []) {
2004
+ if (/draco|meshopt/i.test(ext)) throw new Error('[host] gltf: ' + ext + ' is not supported.');
2005
+ }
2006
+ const cache = new Map();
2007
+
2008
+ const accessor = (index) => {
2009
+ if (cache.has(index)) return cache.get(index);
2010
+ const a = json.accessors[index];
2011
+ if (a.sparse) throw new Error('[host] gltf: sparse accessors are not supported.');
2012
+ const n = COMPONENTS[a.type], Type = TYPES[a.componentType], scale = a.normalized && NORMALISED[a.componentType];
2013
+ const data = new (scale ? Float32Array : Type)(a.count * n);
2014
+ if (a.bufferView !== undefined) {
2015
+ const bv = json.bufferViews[a.bufferView];
2016
+ const base = (bv.byteOffset || 0) + (a.byteOffset || 0), size = Type.BYTES_PER_ELEMENT;
2017
+ const stride = bv.byteStride || n * size;
2018
+ const view = new DataView(buffers[bv.buffer]);
2019
+ const get = {
2020
+ 5120: 'getInt8', 5121: 'getUint8', 5122: 'getInt16', 5123: 'getUint16', 5125: 'getUint32', 5126: 'getFloat32',
2021
+ }[a.componentType];
2022
+ for (let i = 0; i < a.count; i++) {
2023
+ for (let c = 0; c < n; c++) {
2024
+ const v = view[get](base + i * stride + c * size, true);
2025
+ data[i * n + c] = scale ? Math.max(v / scale, -1) : v;
2026
+ }
2027
+ }
2028
+ }
2029
+ const out = { numComponents: n, data };
2030
+ cache.set(index, out);
2031
+ return out;
2032
+ };
2033
+
2034
+ // nodes, parents first
2035
+ const src = json.nodes || [], parentOf = new Array(src.length).fill(-1);
2036
+ src.forEach((node, i) => { for (const c of node.children || []) parentOf[c] = i; });
2037
+ const order = [], remap = new Array(src.length).fill(-1);
2038
+ const visit = (i) => { remap[i] = order.length; order.push(i); for (const c of src[i].children || []) visit(c); };
2039
+ src.forEach((_, i) => { if (parentOf[i] < 0) visit(i); });
2040
+
2041
+ const names = [], parents = new Int32Array(order.length), rest = new Float32Array(order.length * 10);
2042
+ const xf = { pos: [0, 0, 0], rot: [0, 0, 0, 1], scl: [1, 1, 1] };
2043
+ order.forEach((old, i) => {
2044
+ const node = src[old];
2045
+ names.push(node.name || '');
2046
+ parents[i] = parentOf[old] < 0 ? -1 : remap[parentOf[old]];
2047
+ let t = node.translation || [0, 0, 0], r = node.rotation || [0, 0, 0, 1], s = node.scale || [1, 1, 1];
2048
+ if (node.matrix) { mat4ToTransform(xf, node.matrix); t = xf.pos; r = xf.rot; s = xf.scl; }
2049
+ rest.set(t, i * 10); rest.set(r, i * 10 + 3); rest.set(s, i * 10 + 7);
2050
+ });
2051
+
2052
+ // parts: one per triangle primitive per node
2053
+ const primitives = (json.meshes || []).map((mesh) => {
2054
+ const out = [];
2055
+ for (const prim of mesh.primitives) {
2056
+ if ((prim.mode ?? 4) !== 4 || prim.attributes.POSITION === undefined) continue;
2057
+ const arrays = {};
2058
+ for (const [semantic, key] of Object.entries(ATTRIBUTES)) {
2059
+ if (prim.attributes[semantic] !== undefined) arrays[key] = accessor(prim.attributes[semantic]);
2060
+ }
2061
+ const count = arrays.position.data.length / 3;
2062
+ const data = prim.indices !== undefined
2063
+ ? Uint32Array.from(accessor(prim.indices).data)
2064
+ : Uint32Array.from({ length: count }, (_, i) => i);
2065
+ arrays.indices = { numComponents: 3, data };
2066
+ const targetNames = (mesh.extras && mesh.extras.targetNames) || [];
2067
+ const targets = (prim.targets || []).map((target, i) => {
2068
+ const t = { name: targetNames[i] || '' };
2069
+ if (target.POSITION !== undefined) t.position = accessor(target.POSITION);
2070
+ if (target.NORMAL !== undefined) t.normal = accessor(target.NORMAL);
2071
+ return t;
2072
+ });
2073
+ const material = prim.material !== undefined ? json.materials[prim.material] : null;
2074
+ const pbr = material && material.pbrMetallicRoughness;
2075
+ out.push({ arrays, targets, color: (pbr && pbr.baseColorFactor ? pbr.baseColorFactor : [1, 1, 1, 1]).slice() });
2076
+ }
2077
+ return { name: mesh.name || '', primitives: out };
2078
+ });
2079
+
2080
+ const parts = [];
2081
+ order.forEach((old, i) => {
2082
+ const node = src[old];
2083
+ if (node.mesh === undefined) return;
2084
+ const mesh = primitives[node.mesh];
2085
+ for (const p of mesh.primitives) {
2086
+ parts.push({ name: mesh.name || names[i], node: i, skin: node.skin ?? -1, mesh: p.arrays, targets: p.targets, color: p.color });
2087
+ }
2088
+ });
2089
+
2090
+ const skins = (json.skins || []).map((skin) => {
2091
+ const joints = Uint16Array.from(skin.joints, j => remap[j]);
2092
+ const inverseBind = skin.inverseBindMatrices !== undefined
2093
+ ? Float32Array.from(accessor(skin.inverseBindMatrices).data)
2094
+ : Float32Array.from({ length: joints.length * 16 }, (_, k) => IDENTITY[k % 16]);
2095
+ return { name: skin.name || '', joints, inverseBind };
2096
+ });
2097
+
2098
+ const clips = (json.animations || []).map((anim) => {
2099
+ let duration = 0;
2100
+ const channels = [];
2101
+ for (const ch of anim.channels) {
2102
+ if (ch.target.node === undefined) continue;
2103
+ const sampler = anim.samplers[ch.sampler];
2104
+ const times = accessor(sampler.input).data, values = accessor(sampler.output).data;
2105
+ if (times.length) duration = Math.max(duration, times[times.length - 1]);
2106
+ channels.push({ node: remap[ch.target.node], path: ch.target.path, interp: sampler.interpolation || 'LINEAR', times, values });
2107
+ }
2108
+ return { name: anim.name || '', duration, channels };
2109
+ });
2110
+
2111
+ return { parts, nodes: { names, parents, rest }, skins, clips };
2112
+ }
2113
+
2114
+ /**
2115
+ * @file Media sources — images, video and Canvas2D rasters for a bridge's textures, models for its buffers.
1927
2116
  * @module host/media
1928
2117
  * @license AGPL-3.0-only
1929
2118
  *
1930
- * A model arrives as arrays for a bridge's buffer upload: loadModel fetches an
1931
- * OBJ file and parses it (host/obj).
2119
+ * A mesh arrives as arrays for a bridge's buffer upload, its bounds beside
2120
+ * them: loadMesh. A model keeps a file's structure — parts, hierarchy, skins,
2121
+ * clips: loadModel. Both read OBJ (host/obj) and glTF 2.0 (host/gltf), or
2122
+ * another format through a parser the caller passes.
1932
2123
  *
1933
2124
  * What a texture upload reads from: an ImageBitmap fetched from a URL, a
1934
2125
  * hidden <video> element from a file or the user's camera, a bitmap drawn
@@ -1955,26 +2146,151 @@ async function loadImage(url, opts) {
1955
2146
  return o.bitmap ? createImageBitmap(blob, o.bitmap) : createImageBitmap(blob);
1956
2147
  }
1957
2148
 
2149
+ const _ONE_NODE = () => ({ names: [''], parents: Int32Array.of(-1), rest: Float32Array.of(0, 0, 0, 0, 0, 0, 1, 1, 1, 1) });
2150
+ const _part = (mesh) => ({ name: '', node: 0, skin: -1, mesh, targets: [], color: [1, 1, 1, 1] });
2151
+
2152
+ // A morph target's deltas expanded through its mesh's indices; a target carries no indices of its own.
2153
+ function _flatTarget(target, indices) {
2154
+ const flat = meshFlatten(target, indices);
2155
+ delete flat.indices;
2156
+ return flat;
2157
+ }
2158
+
2159
+ // The normals option applied to every part, then each mesh's bounds; parts sharing a mesh share the result.
2160
+ // undefined / true the file's normals; smooth ones where a mesh has none
2161
+ // 'smooth' always recomputed, summed per position (tree's meshGroups), so a faceted file smooths
2162
+ // 'flat' always recomputed on the flattened mesh (tree's meshFlatten), morph targets flattened with it
2163
+ // false nothing computed
2164
+ function _finish(model, o) {
2165
+ const mode = o.normals === undefined || o.normals === true ? 'fill' : o.normals;
2166
+ if (mode !== 'fill' && mode !== 'smooth' && mode !== 'flat' && mode !== false) {
2167
+ throw new Error("[host] model: normals is one of 'smooth', 'flat', false, or left out.");
2168
+ }
2169
+ const done = new Map();
2170
+ for (const part of model.parts) {
2171
+ const source = part.mesh;
2172
+ if (done.has(source)) {
2173
+ part.mesh = done.get(source);
2174
+ if (mode === 'flat') part.targets = part.targets.map(t => _flatTarget(t, source.indices.data));
2175
+ continue;
2176
+ }
2177
+ let mesh = source;
2178
+ const bounds = meshBounds({ min: [0, 0, 0], max: [0, 0, 0], center: [0, 0, 0], diag: 0 }, mesh.position.data);
2179
+ if (mode === 'flat') {
2180
+ mesh = meshFlatten(source);
2181
+ const p = mesh.position.data;
2182
+ mesh.normal = { numComponents: 3, data: meshNormals(new Float32Array(p.length), p, mesh.indices.data) };
2183
+ part.targets = part.targets.map(t => _flatTarget(t, source.indices.data));
2184
+ } else if (mode === 'smooth' || (mode === 'fill' && !mesh.normal)) {
2185
+ const p = mesh.position.data;
2186
+ const groups = meshGroups(new Int32Array(p.length / 3), p, 1e-6 * bounds.diag);
2187
+ mesh.normal = { numComponents: 3, data: meshNormals(new Float32Array(p.length), p, mesh.indices.data, { groups }) };
2188
+ }
2189
+ mesh.bounds = bounds;
2190
+ done.set(source, mesh);
2191
+ part.mesh = mesh;
2192
+ }
2193
+ return model;
2194
+ }
2195
+
2196
+ // Fetch and parse into the model shape, before the normals and bounds.
2197
+ async function _load(url, o, what) {
2198
+ const get = async (at, init) => {
2199
+ const res = await fetch(at, init);
2200
+ if (!res.ok) throw new Error('[host] ' + what + ': ' + at + ' → ' + res.status);
2201
+ return res;
2202
+ };
2203
+ if (typeof o.parse === 'function') {
2204
+ const mesh = await o.parse(await (await get(url, o.fetch)).arrayBuffer());
2205
+ return { parts: [_part(mesh)], nodes: _ONE_NODE(), skins: [], clips: [] };
2206
+ }
2207
+ const format = (o.format || (/\.(\w+)(?:[?#].*)?$/.exec(url) || [])[1] || '').toLowerCase();
2208
+ if (format !== 'obj' && format !== 'glb' && format !== 'gltf') {
2209
+ throw new Error('[host] ' + what + ': ' + url + ' → unknown format; opts.format is one of obj, glb, gltf, or pass opts.parse.');
2210
+ }
2211
+ const res = await get(url, o.fetch);
2212
+ if (format === 'obj') return { parts: [_part(parseObj(await res.text()))], nodes: _ONE_NODE(), skins: [], clips: [] };
2213
+ if (format === 'glb') {
2214
+ const { json, bin } = parseGlb(await res.arrayBuffer());
2215
+ return parseGltf(json, [bin]);
2216
+ }
2217
+ const json = await res.json();
2218
+ const base = typeof location !== 'undefined' ? new URL(url, location.href) : url;
2219
+ const resolve = (uri) => {
2220
+ if (/^data:/.test(uri)) return uri;
2221
+ try { return String(new URL(uri, base)); } catch (e) { return url.replace(/[^/]*$/, '') + uri; }
2222
+ };
2223
+ const buffers = await Promise.all((json.buffers || []).map(async b => (await get(resolve(b.uri), o.fetch)).arrayBuffer()));
2224
+ return parseGltf(json, buffers);
2225
+ }
2226
+
2227
+ /**
2228
+ * Fetch a mesh: one object in the arrays shape — position, indices, and
2229
+ * normal, tangent, texcoord, color, joints, weights when the file carries them
2230
+ * — with `bounds`, its extent `{ min, max, center, diag }`, beside them. A
2231
+ * bridge's buffer upload takes it as it is; `size / mesh.bounds.diag` is the
2232
+ * scale that fits any file to `size`.
2233
+ *
2234
+ * Formats: OBJ (host/obj), and glTF 2.0 as `.glb` or `.gltf` (host/gltf) when
2235
+ * the file holds one part. A file of several parts rejects, naming loadModel:
2236
+ * a mesh is never silently the first of many. `opts.parse` reads any other
2237
+ * format: it receives the fetched ArrayBuffer and returns the arrays shape.
2238
+ *
2239
+ * Normals. Left out, the option keeps the file's normals and gives smooth
2240
+ * ones to a mesh that has none. 'smooth' always recomputes them, summed per
2241
+ * position, so a file that arrives faceted or cut along a texture seam
2242
+ * smooths too. 'flat' always recomputes them on the flattened mesh — every
2243
+ * triangle owning its vertices, so the vertex count grows and the indices
2244
+ * count up. false computes nothing. The procedure, its limits (no crease
2245
+ * angle; grouping by grid cell, at 1e-6 of the bounds' diagonal) and its
2246
+ * sources are tree/mesh's. glTF viewers compute flat normals for a file
2247
+ * without any; here that is `normals: 'flat'`.
2248
+ *
2249
+ * @param {string} url
2250
+ * @param {{ fetch?:object, format?:string, parse?:function(ArrayBuffer):object,
2251
+ * normals?:'smooth'|'flat'|false }} [opts] fetch: the fetch init. format: 'obj' |
2252
+ * 'glb' | 'gltf' when the URL's extension does not say. parse: a parser for another
2253
+ * format. normals: as above.
2254
+ * @returns {Promise<object>} The mesh.
2255
+ */
2256
+ async function loadMesh(url, opts) {
2257
+ const o = opts || {};
2258
+ const model = await _load(url, o, 'mesh');
2259
+ if (model.parts.length !== 1) {
2260
+ throw new Error('[host] mesh: ' + url + ' → ' + model.parts.length + ' parts; loadModel keeps them apart.');
2261
+ }
2262
+ return _finish(model, o).parts[0].mesh;
2263
+ }
2264
+
1958
2265
  /**
1959
- * Fetch an OBJ model into the arrays shape: position and indices, plus normal
1960
- * and texcoord when the file carries them. twgl's createBufferInfoFromArrays
1961
- * takes the result as it is.
2266
+ * Fetch a model with its structure — what a rig, morph targets or animation
2267
+ * need; a single mesh is loadMesh's:
2268
+ *
2269
+ * { parts: [{ name, node, skin, mesh, targets, color }], nodes, skins, clips }
1962
2270
  *
1963
- * Faces are triangulated and each distinct position / uv / normal triple
1964
- * becomes one vertex. Materials are ignored.
2271
+ * A part is one triangle primitive under one node: `mesh` as loadMesh returns
2272
+ * it (the arrays shape with `bounds`, in the part's own space), `targets` its
2273
+ * morph targets as delta arrays, `color` its base colour [r, g, b, a], `node`
2274
+ * the node it hangs from and `skin` its skin's index or −1. `nodes` is the
2275
+ * hierarchy `{ names, parents, rest }`, parents first, `rest` a pose of ten
2276
+ * numbers per node; `skins` are `{ name, joints, inverseBind }` and `clips`
2277
+ * `{ name, duration, channels }` — what tree's clipSample, poseWorld and
2278
+ * jointPalette take. A rigid part draws under its node's world matrix, a
2279
+ * skinned one under its skin's joint palette. An OBJ file is one white part
2280
+ * under one identity node.
2281
+ *
2282
+ * The options are loadMesh's; `normals` applies to every part, 'flat'
2283
+ * flattening each part's morph targets with its mesh.
1965
2284
  *
1966
2285
  * @param {string} url
1967
- * @param {{ fetch?:object }} [opts] fetch: the fetch init (credentials, mode, …).
1968
- * @returns {Promise<{ position:{numComponents:number,data:Float32Array},
1969
- * normal?:{numComponents:number,data:Float32Array},
1970
- * texcoord?:{numComponents:number,data:Float32Array},
1971
- * indices:{numComponents:number,data:Uint32Array} }>}
2286
+ * @param {{ fetch?:object, format?:string, parse?:function(ArrayBuffer):object,
2287
+ * normals?:'smooth'|'flat'|false }} [opts]
2288
+ * @returns {Promise<{ parts:object[], nodes:{ names:string[], parents:Int32Array, rest:Float32Array },
2289
+ * skins:object[], clips:object[] }>}
1972
2290
  */
1973
2291
  async function loadModel(url, opts) {
1974
2292
  const o = opts || {};
1975
- const res = await fetch(url, o.fetch);
1976
- if (!res.ok) throw new Error('[host] model: ' + url + ' → ' + res.status);
1977
- return parseObj(await res.text());
2293
+ return _finish(await _load(url, o, 'model'), o);
1978
2294
  }
1979
2295
 
1980
2296
  /**
@@ -2478,7 +2794,7 @@ function createOrbit(host, cam, opts) {
2478
2794
  * host.cameraHelm(cam, opts) · host.poseHelm(opts)
2479
2795
  * host.poseTrack(opts) · host.cameraTrack(cam, opts)
2480
2796
  * host.hid(opts) · host.gamepad(opts)
2481
- * host.image(url) · host.video(opts) · host.raster(draw, w, h) · host.model(url)
2797
+ * host.image(url) · host.video(opts) · host.raster(draw, w, h) · host.mesh(url) · host.model(url)
2482
2798
  * host.labels // the DOM label layer, created on first access
2483
2799
  * host.orbit(cam, opts)
2484
2800
  * host.tick(dt) // external-loop mode
@@ -2573,7 +2889,9 @@ function createHost(canvas, opts) {
2573
2889
  video(opts) { return host.register(createVideo(opts)); },
2574
2890
  /** Draw through a 2D context into a texture source — see host/media. */
2575
2891
  raster(draw, w, h, opts) { return raster(draw, w, h, opts); },
2576
- /** Fetch an OBJ model into the arrays shape — see host/media. */
2892
+ /** Fetch one mesh — the arrays shape with its bounds — from an OBJ or glTF file, or any format through opts.parse — see host/media. */
2893
+ mesh(url, opts) { return loadMesh(url, opts); },
2894
+ /** Fetch a model with its structure: parts, nodes, skins, clips — see host/media. */
2577
2895
  model(url, opts) { return loadModel(url, opts); },
2578
2896
  /**
2579
2897
  * The label layer — see host/labels. Created on first access, so a
@@ -2630,5 +2948,5 @@ function createHost(canvas, opts) {
2630
2948
  return host;
2631
2949
  }
2632
2950
 
2633
- 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, loadModel, measureCanvas, observeCanvas, poseHelm, poseTrack, raster, validConstraint };
2951
+ 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 };
2634
2952
  //# sourceMappingURL=index.js.map