@nakednous/host 0.0.4 → 0.0.5

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
@@ -6,7 +6,7 @@ device streams (WebHID, Gamepad), image and video sources, a DOM label overlay a
6
6
  **zero renderer**, pure vanilla DOM.
7
7
 
8
8
  > **Status: 0.0.x.** The whole surface below is shipped. What a bridge draws (a handle's locus,
9
- > a track's path, a helm's rig) lives in `twgl.tree` / `p5.tree`; the host only computes, through
9
+ > a track's path, a helm's rig) lives in `webgl.tree` / `p5.tree`; the host only computes, through
10
10
  > `@nakednous/tree`, and listens.
11
11
 
12
12
  ---
@@ -46,7 +46,7 @@ WebGL or WebGPU — it owns a canvas element's input and the frame clock, and ha
46
46
  application
47
47
  │
48
48
  ▼
49
- twgl.tree · webgpu.tree ← bridge: the GPU, thinly
49
+ webgl.tree · webgpu.tree ← bridge: the GPU, thinly
50
50
  │
51
51
  ├── @nakednous/host ← this package: pointer, handles, tracks, helms, players, streams, media, labels, orbit
52
52
  │ │
@@ -82,9 +82,9 @@ 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; parsed by `webgl-obj-loader`, materials ignored |
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 |
86
86
  | `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
- | `host.orbit(cam, opts)` | the fall-through gesture — one pointer orbits, two pan and dolly, the wheel dollies; `update()` reports whether the camera moved; the vertical sense follows the bag's projection (y-up GL, y-flipped p5) |
87
+ | `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
88
  | `host.tick(dt)` | external-loop mode: tick the players; the caller flushes the pointer once its consumers ran |
89
89
  | `host.register(c)` · `host.unregister(c)` · `host.dispose()` | the lifecycle |
90
90
 
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, 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, 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.
@@ -401,7 +401,7 @@ function createPlayers() {
401
401
  return players;
402
402
  }
403
403
 
404
- const _now$1 = () => (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
404
+ const _now$2 = () => (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
405
405
 
406
406
  /**
407
407
  * Create a frame loop. `onFrame(dt)` runs once per animation frame with the
@@ -417,7 +417,7 @@ function createLoop(opts) {
417
417
  const o = opts || {};
418
418
  const raf = o.raf || ((cb) => requestAnimationFrame(cb));
419
419
  const caf = o.caf || ((id) => cancelAnimationFrame(id));
420
- const now = o.now || _now$1;
420
+ const now = o.now || _now$2;
421
421
  const clamp = (o.clampMs ?? 50) / 1000;
422
422
  let handle = null, last = 0;
423
423
  const loop = {
@@ -1847,51 +1847,80 @@ function createGamepad(host, opts) {
1847
1847
  return host.register(s);
1848
1848
  }
1849
1849
 
1850
- function getDefaultExportFromCjs (x) {
1851
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
1852
- }
1850
+ /**
1851
+ * @file OBJ text → the arrays shape a bridge's buffer upload takes.
1852
+ * @module host/obj
1853
+ * @license AGPL-3.0-only
1854
+ *
1855
+ * Positions, texture coordinates and normals. Faces are fan-triangulated — a
1856
+ * quad as 0 1 2 · 2 3 0 — and each distinct position / uv / normal triple
1857
+ * becomes one vertex, in order of first use. Indices are 1-based, or negative
1858
+ * counting back from the last declared element. Groups, objects, smoothing and
1859
+ * materials are ignored.
1860
+ */
1853
1861
 
1854
- var webglObjLoader_min$1 = {exports: {}};
1855
-
1856
- var webglObjLoader_min = webglObjLoader_min$1.exports;
1857
-
1858
- var hasRequiredWebglObjLoader_min;
1859
-
1860
- function requireWebglObjLoader_min () {
1861
- if (hasRequiredWebglObjLoader_min) return webglObjLoader_min$1.exports;
1862
- hasRequiredWebglObjLoader_min = 1;
1863
- (function (module, exports) {
1864
- !function(e,t){module.exports=t();}("undefined"!=typeof self?self:webglObjLoader_min,(function(){return function(e){var t={};function n(a){if(t[a])return t[a].exports;var s=t[a]={i:a,l:false,exports:{}};return e[a].call(s.exports,s,s.exports,n),s.l=true,s.exports}return n.m=e,n.c=t,n.d=function(e,t,a){n.o(e,t)||Object.defineProperty(e,t,{enumerable:true,get:a});},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:true});},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var a=Object.create(null);if(n.r(a),Object.defineProperty(a,"default",{enumerable:true,value:e}),2&t&&"string"!=typeof e)for(var s in e)n.d(a,s,function(t){return e[t]}.bind(null,s));return a},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=0)}({"./src/index.ts":
1865
- /*!**********************!*\
1866
- !*** ./src/index.ts ***!
1867
- \**********************/
1868
- /*! exports provided: OBJ, Attribute, DuplicateAttributeException, Layout, Material, MaterialLibrary, Mesh, TYPES, downloadModels, downloadMeshes, initMeshBuffers, deleteMeshBuffers, version */function(module,__webpack_exports__,__webpack_require__){eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "OBJ", function() { return OBJ; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "version", function() { return version; });\n/* harmony import */ var _mesh__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./mesh */ "./src/mesh.ts");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Mesh", function() { return _mesh__WEBPACK_IMPORTED_MODULE_0__["default"]; });\n\n/* harmony import */ var _material__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./material */ "./src/material.ts");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Material", function() { return _material__WEBPACK_IMPORTED_MODULE_1__["Material"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "MaterialLibrary", function() { return _material__WEBPACK_IMPORTED_MODULE_1__["MaterialLibrary"]; });\n\n/* harmony import */ var _layout__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./layout */ "./src/layout.ts");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Attribute", function() { return _layout__WEBPACK_IMPORTED_MODULE_2__["Attribute"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "DuplicateAttributeException", function() { return _layout__WEBPACK_IMPORTED_MODULE_2__["DuplicateAttributeException"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Layout", function() { return _layout__WEBPACK_IMPORTED_MODULE_2__["Layout"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "TYPES", function() { return _layout__WEBPACK_IMPORTED_MODULE_2__["TYPES"]; });\n\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "downloadModels", function() { return _utils__WEBPACK_IMPORTED_MODULE_3__["downloadModels"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "downloadMeshes", function() { return _utils__WEBPACK_IMPORTED_MODULE_3__["downloadMeshes"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "initMeshBuffers", function() { return _utils__WEBPACK_IMPORTED_MODULE_3__["initMeshBuffers"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "deleteMeshBuffers", function() { return _utils__WEBPACK_IMPORTED_MODULE_3__["deleteMeshBuffers"]; });\n\n\n\n\n\nconst version = "2.0.3";\nconst OBJ = {\n Attribute: _layout__WEBPACK_IMPORTED_MODULE_2__["Attribute"],\n DuplicateAttributeException: _layout__WEBPACK_IMPORTED_MODULE_2__["DuplicateAttributeException"],\n Layout: _layout__WEBPACK_IMPORTED_MODULE_2__["Layout"],\n Material: _material__WEBPACK_IMPORTED_MODULE_1__["Material"],\n MaterialLibrary: _material__WEBPACK_IMPORTED_MODULE_1__["MaterialLibrary"],\n Mesh: _mesh__WEBPACK_IMPORTED_MODULE_0__["default"],\n TYPES: _layout__WEBPACK_IMPORTED_MODULE_2__["TYPES"],\n downloadModels: _utils__WEBPACK_IMPORTED_MODULE_3__["downloadModels"],\n downloadMeshes: _utils__WEBPACK_IMPORTED_MODULE_3__["downloadMeshes"],\n initMeshBuffers: _utils__WEBPACK_IMPORTED_MODULE_3__["initMeshBuffers"],\n deleteMeshBuffers: _utils__WEBPACK_IMPORTED_MODULE_3__["deleteMeshBuffers"],\n version,\n};\n/**\n * @namespace\n */\n\n\n\n//# sourceURL=webpack:///./src/index.ts?');},"./src/layout.ts":
1869
- /*!***********************!*\
1870
- !*** ./src/layout.ts ***!
1871
- \***********************/
1872
- /*! exports provided: TYPES, DuplicateAttributeException, Attribute, Layout */function(module,__webpack_exports__,__webpack_require__){eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TYPES", function() { return TYPES; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DuplicateAttributeException", function() { return DuplicateAttributeException; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Attribute", function() { return Attribute; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Layout", function() { return Layout; });\nvar TYPES;\n(function (TYPES) {\n TYPES["BYTE"] = "BYTE";\n TYPES["UNSIGNED_BYTE"] = "UNSIGNED_BYTE";\n TYPES["SHORT"] = "SHORT";\n TYPES["UNSIGNED_SHORT"] = "UNSIGNED_SHORT";\n TYPES["FLOAT"] = "FLOAT";\n})(TYPES || (TYPES = {}));\n/**\n * An exception for when two or more of the same attributes are found in the\n * same layout.\n * @private\n */\nclass DuplicateAttributeException extends Error {\n /**\n * Create a DuplicateAttributeException\n * @param {Attribute} attribute - The attribute that was found more than\n * once in the {@link Layout}\n */\n constructor(attribute) {\n super(`found duplicate attribute: ${attribute.key}`);\n }\n}\n/**\n * Represents how a vertex attribute should be packed into an buffer.\n * @private\n */\nclass Attribute {\n /**\n * Create an attribute. Do not call this directly, use the predefined\n * constants.\n * @param {string} key - The name of this attribute as if it were a key in\n * an Object. Use the camel case version of the upper snake case\n * const name.\n * @param {number} size - The number of components per vertex attribute.\n * Must be 1, 2, 3, or 4.\n * @param {string} type - The data type of each component for this\n * attribute. Possible values:<br/>\n * "BYTE": signed 8-bit integer, with values in [-128, 127]<br/>\n * "SHORT": signed 16-bit integer, with values in\n * [-32768, 32767]<br/>\n * "UNSIGNED_BYTE": unsigned 8-bit integer, with values in\n * [0, 255]<br/>\n * "UNSIGNED_SHORT": unsigned 16-bit integer, with values in\n * [0, 65535]<br/>\n * "FLOAT": 32-bit floating point number\n * @param {boolean} normalized - Whether integer data values should be\n * normalized when being casted to a float.<br/>\n * If true, signed integers are normalized to [-1, 1].<br/>\n * If true, unsigned integers are normalized to [0, 1].<br/>\n * For type "FLOAT", this parameter has no effect.\n */\n constructor(key, size, type, normalized = false) {\n this.key = key;\n this.size = size;\n this.type = type;\n this.normalized = normalized;\n switch (type) {\n case "BYTE":\n case "UNSIGNED_BYTE":\n this.sizeOfType = 1;\n break;\n case "SHORT":\n case "UNSIGNED_SHORT":\n this.sizeOfType = 2;\n break;\n case "FLOAT":\n this.sizeOfType = 4;\n break;\n default:\n throw new Error(`Unknown gl type: ${type}`);\n }\n this.sizeInBytes = this.sizeOfType * size;\n }\n}\n/**\n * A class to represent the memory layout for a vertex attribute array. Used by\n * {@link Mesh}\'s TBD(...) method to generate a packed array from mesh data.\n * <p>\n * Layout can sort of be thought of as a C-style struct declaration.\n * {@link Mesh}\'s TBD(...) method will use the {@link Layout} instance to\n * pack an array in the given attribute order.\n * <p>\n * Layout also is very helpful when calling a WebGL context\'s\n * <code>vertexAttribPointer</code> method. If you\'ve created a buffer using\n * a Layout instance, then the same Layout instance can be used to determine\n * the size, type, normalized, stride, and offset parameters for\n * <code>vertexAttribPointer</code>.\n * <p>\n * For example:\n * <pre><code>\n *\n * const index = glctx.getAttribLocation(shaderProgram, "pos");\n * glctx.vertexAttribPointer(\n * layout.position.size,\n * glctx[layout.position.type],\n * layout.position.normalized,\n * layout.position.stride,\n * layout.position.offset);\n * </code></pre>\n * @see {@link Mesh}\n */\nclass Layout {\n /**\n * Create a Layout object. This constructor will throw if any duplicate\n * attributes are given.\n * @param {Array} ...attributes - An ordered list of attributes that\n * describe the desired memory layout for each vertex attribute.\n * <p>\n *\n * @see {@link Mesh}\n */\n constructor(...attributes) {\n this.attributes = attributes;\n this.attributeMap = {};\n let offset = 0;\n let maxStrideMultiple = 0;\n for (const attribute of attributes) {\n if (this.attributeMap[attribute.key]) {\n throw new DuplicateAttributeException(attribute);\n }\n // Add padding to satisfy WebGL\'s requirement that all\n // vertexAttribPointer calls have an offset that is a multiple of\n // the type size.\n if (offset % attribute.sizeOfType !== 0) {\n offset += attribute.sizeOfType - (offset % attribute.sizeOfType);\n console.warn("Layout requires padding before " + attribute.key + " attribute");\n }\n this.attributeMap[attribute.key] = {\n attribute: attribute,\n size: attribute.size,\n type: attribute.type,\n normalized: attribute.normalized,\n offset: offset,\n };\n offset += attribute.sizeInBytes;\n maxStrideMultiple = Math.max(maxStrideMultiple, attribute.sizeOfType);\n }\n // Add padding to the end to satisfy WebGL\'s requirement that all\n // vertexAttribPointer calls have a stride that is a multiple of the\n // type size. Because we\'re putting differently sized attributes into\n // the same buffer, it must be padded to a multiple of the largest\n // type size.\n if (offset % maxStrideMultiple !== 0) {\n offset += maxStrideMultiple - (offset % maxStrideMultiple);\n console.warn("Layout requires padding at the back");\n }\n this.stride = offset;\n for (const attribute of attributes) {\n this.attributeMap[attribute.key].stride = this.stride;\n }\n }\n}\n// Geometry attributes\n/**\n * Attribute layout to pack a vertex\'s x, y, & z as floats\n *\n * @see {@link Layout}\n */\nLayout.POSITION = new Attribute("position", 3, TYPES.FLOAT);\n/**\n * Attribute layout to pack a vertex\'s normal\'s x, y, & z as floats\n *\n * @see {@link Layout}\n */\nLayout.NORMAL = new Attribute("normal", 3, TYPES.FLOAT);\n/**\n * Attribute layout to pack a vertex\'s normal\'s x, y, & z as floats.\n * <p>\n * This value will be computed on-the-fly based on the texture coordinates.\n * If no texture coordinates are available, the generated value will default to\n * 0, 0, 0.\n *\n * @see {@link Layout}\n */\nLayout.TANGENT = new Attribute("tangent", 3, TYPES.FLOAT);\n/**\n * Attribute layout to pack a vertex\'s normal\'s bitangent x, y, & z as floats.\n * <p>\n * This value will be computed on-the-fly based on the texture coordinates.\n * If no texture coordinates are available, the generated value will default to\n * 0, 0, 0.\n * @see {@link Layout}\n */\nLayout.BITANGENT = new Attribute("bitangent", 3, TYPES.FLOAT);\n/**\n * Attribute layout to pack a vertex\'s texture coordinates\' u & v as floats\n *\n * @see {@link Layout}\n */\nLayout.UV = new Attribute("uv", 2, TYPES.FLOAT);\n// Material attributes\n/**\n * Attribute layout to pack an unsigned short to be interpreted as a the index\n * into a {@link Mesh}\'s materials list.\n * <p>\n * The intention of this value is to send all of the {@link Mesh}\'s materials\n * into multiple shader uniforms and then reference the current one by this\n * vertex attribute.\n * <p>\n * example glsl code:\n *\n * <pre><code>\n * // this is bound using MATERIAL_INDEX\n * attribute int materialIndex;\n *\n * struct Material {\n * vec3 diffuse;\n * vec3 specular;\n * vec3 specularExponent;\n * };\n *\n * uniform Material materials[MAX_MATERIALS];\n *\n * // ...\n *\n * vec3 diffuse = materials[materialIndex];\n *\n * </code></pre>\n * TODO: More description & test to make sure subscripting by attributes even\n * works for webgl\n *\n * @see {@link Layout}\n */\nLayout.MATERIAL_INDEX = new Attribute("materialIndex", 1, TYPES.SHORT);\nLayout.MATERIAL_ENABLED = new Attribute("materialEnabled", 1, TYPES.UNSIGNED_SHORT);\nLayout.AMBIENT = new Attribute("ambient", 3, TYPES.FLOAT);\nLayout.DIFFUSE = new Attribute("diffuse", 3, TYPES.FLOAT);\nLayout.SPECULAR = new Attribute("specular", 3, TYPES.FLOAT);\nLayout.SPECULAR_EXPONENT = new Attribute("specularExponent", 3, TYPES.FLOAT);\nLayout.EMISSIVE = new Attribute("emissive", 3, TYPES.FLOAT);\nLayout.TRANSMISSION_FILTER = new Attribute("transmissionFilter", 3, TYPES.FLOAT);\nLayout.DISSOLVE = new Attribute("dissolve", 1, TYPES.FLOAT);\nLayout.ILLUMINATION = new Attribute("illumination", 1, TYPES.UNSIGNED_SHORT);\nLayout.REFRACTION_INDEX = new Attribute("refractionIndex", 1, TYPES.FLOAT);\nLayout.SHARPNESS = new Attribute("sharpness", 1, TYPES.FLOAT);\nLayout.MAP_DIFFUSE = new Attribute("mapDiffuse", 1, TYPES.SHORT);\nLayout.MAP_AMBIENT = new Attribute("mapAmbient", 1, TYPES.SHORT);\nLayout.MAP_SPECULAR = new Attribute("mapSpecular", 1, TYPES.SHORT);\nLayout.MAP_SPECULAR_EXPONENT = new Attribute("mapSpecularExponent", 1, TYPES.SHORT);\nLayout.MAP_DISSOLVE = new Attribute("mapDissolve", 1, TYPES.SHORT);\nLayout.ANTI_ALIASING = new Attribute("antiAliasing", 1, TYPES.UNSIGNED_SHORT);\nLayout.MAP_BUMP = new Attribute("mapBump", 1, TYPES.SHORT);\nLayout.MAP_DISPLACEMENT = new Attribute("mapDisplacement", 1, TYPES.SHORT);\nLayout.MAP_DECAL = new Attribute("mapDecal", 1, TYPES.SHORT);\nLayout.MAP_EMISSIVE = new Attribute("mapEmissive", 1, TYPES.SHORT);\n\n\n//# sourceURL=webpack:///./src/layout.ts?');},"./src/material.ts":
1873
- /*!*************************!*\
1874
- !*** ./src/material.ts ***!
1875
- \*************************/
1876
- /*! exports provided: Material, MaterialLibrary */function(module,__webpack_exports__,__webpack_require__){eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Material", function() { return Material; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MaterialLibrary", function() { return MaterialLibrary; });\n/**\n * The Material class.\n */\nclass Material {\n constructor(name) {\n this.name = name;\n /**\n * Constructor\n * @param {String} name the unique name of the material\n */\n // The values for the following attibutes\n // are an array of R, G, B normalized values.\n // Ka - Ambient Reflectivity\n this.ambient = [0, 0, 0];\n // Kd - Defuse Reflectivity\n this.diffuse = [0, 0, 0];\n // Ks\n this.specular = [0, 0, 0];\n // Ke\n this.emissive = [0, 0, 0];\n // Tf\n this.transmissionFilter = [0, 0, 0];\n // d\n this.dissolve = 0;\n // valid range is between 0 and 1000\n this.specularExponent = 0;\n // either d or Tr; valid values are normalized\n this.transparency = 0;\n // illum - the enum of the illumination model to use\n this.illumination = 0;\n // Ni - Set to "normal" (air).\n this.refractionIndex = 1;\n // sharpness\n this.sharpness = 0;\n // map_Kd\n this.mapDiffuse = emptyTextureOptions();\n // map_Ka\n this.mapAmbient = emptyTextureOptions();\n // map_Ks\n this.mapSpecular = emptyTextureOptions();\n // map_Ns\n this.mapSpecularExponent = emptyTextureOptions();\n // map_d\n this.mapDissolve = emptyTextureOptions();\n // map_aat\n this.antiAliasing = false;\n // map_bump or bump\n this.mapBump = emptyTextureOptions();\n // disp\n this.mapDisplacement = emptyTextureOptions();\n // decal\n this.mapDecal = emptyTextureOptions();\n // map_Ke\n this.mapEmissive = emptyTextureOptions();\n // refl - when the reflection type is a cube, there will be multiple refl\n // statements for each side of the cube. If it\'s a spherical\n // reflection, there should only ever be one.\n this.mapReflections = [];\n }\n}\nconst SENTINEL_MATERIAL = new Material("sentinel");\n/**\n * https://en.wikipedia.org/wiki/Wavefront_.obj_file\n * http://paulbourke.net/dataformats/mtl/\n */\nclass MaterialLibrary {\n constructor(data) {\n this.data = data;\n /**\n * Constructs the Material Parser\n * @param mtlData the MTL file contents\n */\n this.currentMaterial = SENTINEL_MATERIAL;\n this.materials = {};\n this.parse();\n }\n /* eslint-disable camelcase */\n /* the function names here disobey camelCase conventions\n to make parsing/routing easier. see the parse function\n documentation for more information. */\n /**\n * Creates a new Material object and adds to the registry.\n * @param tokens the tokens associated with the directive\n */\n parse_newmtl(tokens) {\n const name = tokens[0];\n // console.info(\'Parsing new Material:\', name);\n this.currentMaterial = new Material(name);\n this.materials[name] = this.currentMaterial;\n }\n /**\n * See the documenation for parse_Ka below for a better understanding.\n *\n * Given a list of possible color tokens, returns an array of R, G, and B\n * color values.\n *\n * @param tokens the tokens associated with the directive\n * @return {*} a 3 element array containing the R, G, and B values\n * of the color.\n */\n parseColor(tokens) {\n if (tokens[0] == "spectral") {\n throw new Error("The MTL parser does not support spectral curve files. You will " +\n "need to convert the MTL colors to either RGB or CIEXYZ.");\n }\n if (tokens[0] == "xyz") {\n throw new Error("The MTL parser does not currently support XYZ colors. Either convert the " +\n "XYZ values to RGB or create an issue to add support for XYZ");\n }\n // from my understanding of the spec, RGB values at this point\n // will either be 3 floats or exactly 1 float, so that\'s the check\n // that i\'m going to perform here\n if (tokens.length == 3) {\n const [x, y, z] = tokens;\n return [parseFloat(x), parseFloat(y), parseFloat(z)];\n }\n // Since tokens at this point has a length of 3, we\'re going to assume\n // it\'s exactly 1, skipping the check for 2.\n const value = parseFloat(tokens[0]);\n // in this case, all values are equivalent\n return [value, value, value];\n }\n /**\n * Parse the ambient reflectivity\n *\n * A Ka directive can take one of three forms:\n * - Ka r g b\n * - Ka spectral file.rfl\n * - Ka xyz x y z\n * These three forms are mutually exclusive in that only one\n * declaration can exist per material. It is considered a syntax\n * error otherwise.\n *\n * The "Ka" form specifies the ambient reflectivity using RGB values.\n * The "g" and "b" values are optional. If only the "r" value is\n * specified, then the "g" and "b" values are assigned the value of\n * "r". Values are normally in the range 0.0 to 1.0. Values outside\n * of this range increase or decrease the reflectivity accordingly.\n *\n * The "Ka spectral" form specifies the ambient reflectivity using a\n * spectral curve. "file.rfl" is the name of the ".rfl" file containing\n * the curve data. "factor" is an optional argument which is a multiplier\n * for the values in the .rfl file and defaults to 1.0 if not specified.\n *\n * The "Ka xyz" form specifies the ambient reflectivity using CIEXYZ values.\n * "x y z" are the values of the CIEXYZ color space. The "y" and "z" arguments\n * are optional and take on the value of the "x" component if only "x" is\n * specified. The "x y z" values are normally in the range of 0.0 to 1.0 and\n * increase or decrease ambient reflectivity accordingly outside of that\n * range.\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Ka(tokens) {\n this.currentMaterial.ambient = this.parseColor(tokens);\n }\n /**\n * Diffuse Reflectivity\n *\n * Similar to the Ka directive. Simply replace "Ka" with "Kd" and the rules\n * are the same\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Kd(tokens) {\n this.currentMaterial.diffuse = this.parseColor(tokens);\n }\n /**\n * Spectral Reflectivity\n *\n * Similar to the Ka directive. Simply replace "Ks" with "Kd" and the rules\n * are the same\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Ks(tokens) {\n this.currentMaterial.specular = this.parseColor(tokens);\n }\n /**\n * Emissive\n *\n * The amount and color of light emitted by the object.\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Ke(tokens) {\n this.currentMaterial.emissive = this.parseColor(tokens);\n }\n /**\n * Transmission Filter\n *\n * Any light passing through the object is filtered by the transmission\n * filter, which only allows specific colors to pass through. For example, Tf\n * 0 1 0 allows all of the green to pass through and filters out all of the\n * red and blue.\n *\n * Similar to the Ka directive. Simply replace "Ks" with "Tf" and the rules\n * are the same\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Tf(tokens) {\n this.currentMaterial.transmissionFilter = this.parseColor(tokens);\n }\n /**\n * Specifies the dissolve for the current material.\n *\n * Statement: d [-halo] `factor`\n *\n * Example: "d 0.5"\n *\n * The factor is the amount this material dissolves into the background. A\n * factor of 1.0 is fully opaque. This is the default when a new material is\n * created. A factor of 0.0 is fully dissolved (completely transparent).\n *\n * Unlike a real transparent material, the dissolve does not depend upon\n * material thickness nor does it have any spectral character. Dissolve works\n * on all illumination models.\n *\n * The dissolve statement allows for an optional "-halo" flag which indicates\n * that a dissolve is dependent on the surface orientation relative to the\n * viewer. For example, a sphere with the following dissolve, "d -halo 0.0",\n * will be fully dissolved at its center and will appear gradually more opaque\n * toward its edge.\n *\n * "factor" is the minimum amount of dissolve applied to the material. The\n * amount of dissolve will vary between 1.0 (fully opaque) and the specified\n * "factor". The formula is:\n *\n * dissolve = 1.0 - (N*v)(1.0-factor)\n *\n * @param tokens the tokens associated with the directive\n */\n parse_d(tokens) {\n // this ignores the -halo option as I can\'t find any documentation on what\n // it\'s supposed to be.\n this.currentMaterial.dissolve = parseFloat(tokens.pop() || "0");\n }\n /**\n * The "illum" statement specifies the illumination model to use in the\n * material. Illumination models are mathematical equations that represent\n * various material lighting and shading effects.\n *\n * The illumination number can be a number from 0 to 10. The following are\n * the list of illumination enumerations and their summaries:\n * 0. Color on and Ambient off\n * 1. Color on and Ambient on\n * 2. Highlight on\n * 3. Reflection on and Ray trace on\n * 4. Transparency: Glass on, Reflection: Ray trace on\n * 5. Reflection: Fresnel on and Ray trace on\n * 6. Transparency: Refraction on, Reflection: Fresnel off and Ray trace on\n * 7. Transparency: Refraction on, Reflection: Fresnel on and Ray trace on\n * 8. Reflection on and Ray trace off\n * 9. Transparency: Glass on, Reflection: Ray trace off\n * 10. Casts shadows onto invisible surfaces\n *\n * Example: "illum 2" to specify the "Highlight on" model\n *\n * @param tokens the tokens associated with the directive\n */\n parse_illum(tokens) {\n this.currentMaterial.illumination = parseInt(tokens[0]);\n }\n /**\n * Optical Density (AKA Index of Refraction)\n *\n * Statement: Ni `index`\n *\n * Example: Ni 1.0\n *\n * Specifies the optical density for the surface. `index` is the value\n * for the optical density. The values can range from 0.001 to 10. A value of\n * 1.0 means that light does not bend as it passes through an object.\n * Increasing the optical_density increases the amount of bending. Glass has\n * an index of refraction of about 1.5. Values of less than 1.0 produce\n * bizarre results and are not recommended\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Ni(tokens) {\n this.currentMaterial.refractionIndex = parseFloat(tokens[0]);\n }\n /**\n * Specifies the specular exponent for the current material. This defines the\n * focus of the specular highlight.\n *\n * Statement: Ns `exponent`\n *\n * Example: "Ns 250"\n *\n * `exponent` is the value for the specular exponent. A high exponent results\n * in a tight, concentrated highlight. Ns Values normally range from 0 to\n * 1000.\n *\n * @param tokens the tokens associated with the directive\n */\n parse_Ns(tokens) {\n this.currentMaterial.specularExponent = parseInt(tokens[0]);\n }\n /**\n * Specifies the sharpness of the reflections from the local reflection map.\n *\n * Statement: sharpness `value`\n *\n * Example: "sharpness 100"\n *\n * If a material does not have a local reflection map defined in its material\n * defintions, sharpness will apply to the global reflection map defined in\n * PreView.\n *\n * `value` can be a number from 0 to 1000. The default is 60. A high value\n * results in a clear reflection of objects in the reflection map.\n *\n * Tip: sharpness values greater than 100 introduce aliasing effects in\n * flat surfaces that are viewed at a sharp angle.\n *\n * @param tokens the tokens associated with the directive\n */\n parse_sharpness(tokens) {\n this.currentMaterial.sharpness = parseInt(tokens[0]);\n }\n /**\n * Parses the -cc flag and updates the options object with the values.\n *\n * @param values the values passed to the -cc flag\n * @param options the Object of all image options\n */\n parse_cc(values, options) {\n options.colorCorrection = values[0] == "on";\n }\n /**\n * Parses the -blendu flag and updates the options object with the values.\n *\n * @param values the values passed to the -blendu flag\n * @param options the Object of all image options\n */\n parse_blendu(values, options) {\n options.horizontalBlending = values[0] == "on";\n }\n /**\n * Parses the -blendv flag and updates the options object with the values.\n *\n * @param values the values passed to the -blendv flag\n * @param options the Object of all image options\n */\n parse_blendv(values, options) {\n options.verticalBlending = values[0] == "on";\n }\n /**\n * Parses the -boost flag and updates the options object with the values.\n *\n * @param values the values passed to the -boost flag\n * @param options the Object of all image options\n */\n parse_boost(values, options) {\n options.boostMipMapSharpness = parseFloat(values[0]);\n }\n /**\n * Parses the -mm flag and updates the options object with the values.\n *\n * @param values the values passed to the -mm flag\n * @param options the Object of all image options\n */\n parse_mm(values, options) {\n options.modifyTextureMap.brightness = parseFloat(values[0]);\n options.modifyTextureMap.contrast = parseFloat(values[1]);\n }\n /**\n * Parses and sets the -o, -s, and -t u, v, and w values\n *\n * @param values the values passed to the -o, -s, -t flag\n * @param {Object} option the Object of either the -o, -s, -t option\n * @param {Integer} defaultValue the Object of all image options\n */\n parse_ost(values, option, defaultValue) {\n while (values.length < 3) {\n values.push(defaultValue.toString());\n }\n option.u = parseFloat(values[0]);\n option.v = parseFloat(values[1]);\n option.w = parseFloat(values[2]);\n }\n /**\n * Parses the -o flag and updates the options object with the values.\n *\n * @param values the values passed to the -o flag\n * @param options the Object of all image options\n */\n parse_o(values, options) {\n this.parse_ost(values, options.offset, 0);\n }\n /**\n * Parses the -s flag and updates the options object with the values.\n *\n * @param values the values passed to the -s flag\n * @param options the Object of all image options\n */\n parse_s(values, options) {\n this.parse_ost(values, options.scale, 1);\n }\n /**\n * Parses the -t flag and updates the options object with the values.\n *\n * @param values the values passed to the -t flag\n * @param options the Object of all image options\n */\n parse_t(values, options) {\n this.parse_ost(values, options.turbulence, 0);\n }\n /**\n * Parses the -texres flag and updates the options object with the values.\n *\n * @param values the values passed to the -texres flag\n * @param options the Object of all image options\n */\n parse_texres(values, options) {\n options.textureResolution = parseFloat(values[0]);\n }\n /**\n * Parses the -clamp flag and updates the options object with the values.\n *\n * @param values the values passed to the -clamp flag\n * @param options the Object of all image options\n */\n parse_clamp(values, options) {\n options.clamp = values[0] == "on";\n }\n /**\n * Parses the -bm flag and updates the options object with the values.\n *\n * @param values the values passed to the -bm flag\n * @param options the Object of all image options\n */\n parse_bm(values, options) {\n options.bumpMultiplier = parseFloat(values[0]);\n }\n /**\n * Parses the -imfchan flag and updates the options object with the values.\n *\n * @param values the values passed to the -imfchan flag\n * @param options the Object of all image options\n */\n parse_imfchan(values, options) {\n options.imfChan = values[0];\n }\n /**\n * This only exists for relection maps and denotes the type of reflection.\n *\n * @param values the values passed to the -type flag\n * @param options the Object of all image options\n */\n parse_type(values, options) {\n options.reflectionType = values[0];\n }\n /**\n * Parses the texture\'s options and returns an options object with the info\n *\n * @param tokens all of the option tokens to pass to the texture\n * @return {Object} a complete object of objects to apply to the texture\n */\n parseOptions(tokens) {\n const options = emptyTextureOptions();\n let option;\n let values;\n const optionsToValues = {};\n tokens.reverse();\n while (tokens.length) {\n // token is guaranteed to exists here, hence the explicit "as"\n const token = tokens.pop();\n if (token.startsWith("-")) {\n option = token.substr(1);\n optionsToValues[option] = [];\n }\n else if (option) {\n optionsToValues[option].push(token);\n }\n }\n for (option in optionsToValues) {\n if (!optionsToValues.hasOwnProperty(option)) {\n continue;\n }\n values = optionsToValues[option];\n const optionMethod = this[`parse_${option}`];\n if (optionMethod) {\n optionMethod.bind(this)(values, options);\n }\n }\n return options;\n }\n /**\n * Parses the given texture map line.\n *\n * @param tokens all of the tokens representing the texture\n * @return a complete object of objects to apply to the texture\n */\n parseMap(tokens) {\n // according to wikipedia:\n // (https://en.wikipedia.org/wiki/Wavefront_.obj_file#Vendor_specific_alterations)\n // there is at least one vendor that places the filename before the options\n // rather than after (which is to spec). All options start with a \'-\'\n // so if the first token doesn\'t start with a \'-\', we\'re going to assume\n // it\'s the name of the map file.\n let optionsString;\n let filename = "";\n if (!tokens[0].startsWith("-")) {\n [filename, ...optionsString] = tokens;\n }\n else {\n filename = tokens.pop();\n optionsString = tokens;\n }\n const options = this.parseOptions(optionsString);\n options.filename = filename.replace(/\\\\/g, "/");\n return options;\n }\n /**\n * Parses the ambient map.\n *\n * @param tokens list of tokens for the map_Ka direcive\n */\n parse_map_Ka(tokens) {\n this.currentMaterial.mapAmbient = this.parseMap(tokens);\n }\n /**\n * Parses the diffuse map.\n *\n * @param tokens list of tokens for the map_Kd direcive\n */\n parse_map_Kd(tokens) {\n this.currentMaterial.mapDiffuse = this.parseMap(tokens);\n }\n /**\n * Parses the specular map.\n *\n * @param tokens list of tokens for the map_Ks direcive\n */\n parse_map_Ks(tokens) {\n this.currentMaterial.mapSpecular = this.parseMap(tokens);\n }\n /**\n * Parses the emissive map.\n *\n * @param tokens list of tokens for the map_Ke direcive\n */\n parse_map_Ke(tokens) {\n this.currentMaterial.mapEmissive = this.parseMap(tokens);\n }\n /**\n * Parses the specular exponent map.\n *\n * @param tokens list of tokens for the map_Ns direcive\n */\n parse_map_Ns(tokens) {\n this.currentMaterial.mapSpecularExponent = this.parseMap(tokens);\n }\n /**\n * Parses the dissolve map.\n *\n * @param tokens list of tokens for the map_d direcive\n */\n parse_map_d(tokens) {\n this.currentMaterial.mapDissolve = this.parseMap(tokens);\n }\n /**\n * Parses the anti-aliasing option.\n *\n * @param tokens list of tokens for the map_aat direcive\n */\n parse_map_aat(tokens) {\n this.currentMaterial.antiAliasing = tokens[0] == "on";\n }\n /**\n * Parses the bump map.\n *\n * @param tokens list of tokens for the map_bump direcive\n */\n parse_map_bump(tokens) {\n this.currentMaterial.mapBump = this.parseMap(tokens);\n }\n /**\n * Parses the bump map.\n *\n * @param tokens list of tokens for the bump direcive\n */\n parse_bump(tokens) {\n this.parse_map_bump(tokens);\n }\n /**\n * Parses the disp map.\n *\n * @param tokens list of tokens for the disp direcive\n */\n parse_disp(tokens) {\n this.currentMaterial.mapDisplacement = this.parseMap(tokens);\n }\n /**\n * Parses the decal map.\n *\n * @param tokens list of tokens for the map_decal direcive\n */\n parse_decal(tokens) {\n this.currentMaterial.mapDecal = this.parseMap(tokens);\n }\n /**\n * Parses the refl map.\n *\n * @param tokens list of tokens for the refl direcive\n */\n parse_refl(tokens) {\n this.currentMaterial.mapReflections.push(this.parseMap(tokens));\n }\n /**\n * Parses the MTL file.\n *\n * Iterates line by line parsing each MTL directive.\n *\n * This function expects the first token in the line\n * to be a valid MTL directive. That token is then used\n * to try and run a method on this class. parse_[directive]\n * E.g., the `newmtl` directive would try to call the method\n * parse_newmtl. Each parsing function takes in the remaining\n * list of tokens and updates the currentMaterial class with\n * the attributes provided.\n */\n parse() {\n const lines = this.data.split(/\\r?\\n/);\n for (let line of lines) {\n line = line.trim();\n if (!line || line.startsWith("#")) {\n continue;\n }\n const [directive, ...tokens] = line.split(/\\s/);\n const parseMethod = this[`parse_${directive}`];\n if (!parseMethod) {\n console.warn(`Don\'t know how to parse the directive: "${directive}"`);\n continue;\n }\n // console.log(`Parsing "${directive}" with tokens: ${tokens}`);\n parseMethod.bind(this)(tokens);\n }\n // some cleanup. These don\'t need to be exposed as public data.\n delete this.data;\n this.currentMaterial = SENTINEL_MATERIAL;\n }\n}\nfunction emptyTextureOptions() {\n return {\n colorCorrection: false,\n horizontalBlending: true,\n verticalBlending: true,\n boostMipMapSharpness: 0,\n modifyTextureMap: {\n brightness: 0,\n contrast: 1,\n },\n offset: { u: 0, v: 0, w: 0 },\n scale: { u: 1, v: 1, w: 1 },\n turbulence: { u: 0, v: 0, w: 0 },\n clamp: false,\n textureResolution: null,\n bumpMultiplier: 1,\n imfChan: null,\n filename: "",\n };\n}\n\n\n//# sourceURL=webpack:///./src/material.ts?');},"./src/mesh.ts":
1877
- /*!*********************!*\
1878
- !*** ./src/mesh.ts ***!
1879
- \*********************/
1880
- /*! exports provided: default */function(module,__webpack_exports__,__webpack_require__){eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Mesh; });\n/* harmony import */ var _layout__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./layout */ "./src/layout.ts");\n\n/**\n * The main Mesh class. The constructor will parse through the OBJ file data\n * and collect the vertex, vertex normal, texture, and face information. This\n * information can then be used later on when creating your VBOs. See\n * OBJ.initMeshBuffers for an example of how to use the newly created Mesh\n */\nclass Mesh {\n /**\n * Create a Mesh\n * @param {String} objectData - a string representation of an OBJ file with\n * newlines preserved.\n * @param {Object} options - a JS object containing valid options. See class\n * documentation for options.\n * @param {bool} options.enableWTextureCoord - Texture coordinates can have\n * an optional "w" coordinate after the u and v coordinates. This extra\n * value can be used in order to perform fancy transformations on the\n * textures themselves. Default is to truncate to only the u an v\n * coordinates. Passing true will provide a default value of 0 in the\n * event that any or all texture coordinates don\'t provide a w value.\n * Always use the textureStride attribute in order to determine the\n * stride length of the texture coordinates when rendering the element\n * array.\n * @param {bool} options.calcTangentsAndBitangents - Calculate the tangents\n * and bitangents when loading of the OBJ is completed. This adds two new\n * attributes to the Mesh instance: `tangents` and `bitangents`.\n */\n constructor(objectData, options) {\n this.name = "";\n this.indicesPerMaterial = [];\n this.materialsByIndex = {};\n this.tangents = [];\n this.bitangents = [];\n options = options || {};\n options.materials = options.materials || {};\n options.enableWTextureCoord = !!options.enableWTextureCoord;\n // the list of unique vertex, normal, texture, attributes\n this.vertexNormals = [];\n this.textures = [];\n // the indicies to draw the faces\n this.indices = [];\n this.textureStride = options.enableWTextureCoord ? 3 : 2;\n /*\n The OBJ file format does a sort of compression when saving a model in a\n program like Blender. There are at least 3 sections (4 including textures)\n within the file. Each line in a section begins with the same string:\n * \'v\': indicates vertex section\n * \'vn\': indicates vertex normal section\n * \'f\': indicates the faces section\n * \'vt\': indicates vertex texture section (if textures were used on the model)\n Each of the above sections (except for the faces section) is a list/set of\n unique vertices.\n\n Each line of the faces section contains a list of\n (vertex, [texture], normal) groups.\n\n **Note:** The following documentation will use a capital "V" Vertex to\n denote the above (vertex, [texture], normal) groups whereas a lowercase\n "v" vertex is used to denote an X, Y, Z coordinate.\n\n Some examples:\n // the texture index is optional, both formats are possible for models\n // without a texture applied\n f 1/25 18/46 12/31\n f 1//25 18//46 12//31\n\n // A 3 vertex face with texture indices\n f 16/92/11 14/101/22 1/69/1\n\n // A 4 vertex face\n f 16/92/11 40/109/40 38/114/38 14/101/22\n\n The first two lines are examples of a 3 vertex face without a texture applied.\n The second is an example of a 3 vertex face with a texture applied.\n The third is an example of a 4 vertex face. Note: a face can contain N\n number of vertices.\n\n Each number that appears in one of the groups is a 1-based index\n corresponding to an item from the other sections (meaning that indexing\n starts at one and *not* zero).\n\n For example:\n `f 16/92/11` is saying to\n - take the 16th element from the [v] vertex array\n - take the 92nd element from the [vt] texture array\n - take the 11th element from the [vn] normal array\n and together they make a unique vertex.\n Using all 3+ unique Vertices from the face line will produce a polygon.\n\n Now, you could just go through the OBJ file and create a new vertex for\n each face line and WebGL will draw what appears to be the same model.\n However, vertices will be overlapped and duplicated all over the place.\n\n Consider a cube in 3D space centered about the origin and each side is\n 2 units long. The front face (with the positive Z-axis pointing towards\n you) would have a Top Right vertex (looking orthogonal to its normal)\n mapped at (1,1,1) The right face would have a Top Left vertex (looking\n orthogonal to its normal) at (1,1,1) and the top face would have a Bottom\n Right vertex (looking orthogonal to its normal) at (1,1,1). Each face\n has a vertex at the same coordinates, however, three distinct vertices\n will be drawn at the same spot.\n\n To solve the issue of duplicate Vertices (the `(vertex, [texture], normal)`\n groups), while iterating through the face lines, when a group is encountered\n the whole group string (\'16/92/11\') is checked to see if it exists in the\n packed.hashindices object, and if it doesn\'t, the indices it specifies\n are used to look up each attribute in the corresponding attribute arrays\n already created. The values are then copied to the corresponding unpacked\n array (flattened to play nice with WebGL\'s ELEMENT_ARRAY_BUFFER indexing),\n the group string is added to the hashindices set and the current unpacked\n index is used as this hashindices value so that the group of elements can\n be reused. The unpacked index is incremented. If the group string already\n exists in the hashindices object, its corresponding value is the index of\n that group and is appended to the unpacked indices array.\n */\n const verts = [];\n const vertNormals = [];\n const textures = [];\n const materialNamesByIndex = [];\n const materialIndicesByName = {};\n // keep track of what material we\'ve seen last\n let currentMaterialIndex = -1;\n let currentObjectByMaterialIndex = 0;\n // unpacking stuff\n const unpacked = {\n verts: [],\n norms: [],\n textures: [],\n hashindices: {},\n indices: [[]],\n materialIndices: [],\n index: 0,\n };\n const VERTEX_RE = /^v\\s/;\n const NORMAL_RE = /^vn\\s/;\n const TEXTURE_RE = /^vt\\s/;\n const FACE_RE = /^f\\s/;\n const WHITESPACE_RE = /\\s+/;\n const USE_MATERIAL_RE = /^usemtl/;\n // array of lines separated by the newline\n const lines = objectData.split("\\n");\n for (let line of lines) {\n line = line.trim();\n if (!line || line.startsWith("#")) {\n continue;\n }\n const elements = line.split(WHITESPACE_RE);\n elements.shift();\n if (VERTEX_RE.test(line)) {\n // if this is a vertex\n verts.push(...elements);\n }\n else if (NORMAL_RE.test(line)) {\n // if this is a vertex normal\n vertNormals.push(...elements);\n }\n else if (TEXTURE_RE.test(line)) {\n let coords = elements;\n // by default, the loader will only look at the U and V\n // coordinates of the vt declaration. So, this truncates the\n // elements to only those 2 values. If W texture coordinate\n // support is enabled, then the texture coordinate is\n // expected to have three values in it.\n if (elements.length > 2 && !options.enableWTextureCoord) {\n coords = elements.slice(0, 2);\n }\n else if (elements.length === 2 && options.enableWTextureCoord) {\n // If for some reason W texture coordinate support is enabled\n // and only the U and V coordinates are given, then we supply\n // the default value of 0 so that the stride length is correct\n // when the textures are unpacked below.\n coords.push("0");\n }\n textures.push(...coords);\n }\n else if (USE_MATERIAL_RE.test(line)) {\n const materialName = elements[0];\n // check to see if we\'ve ever seen it before\n if (!(materialName in materialIndicesByName)) {\n // new material we\'ve never seen\n materialNamesByIndex.push(materialName);\n materialIndicesByName[materialName] = materialNamesByIndex.length - 1;\n // push new array into indices\n // already contains an array at index zero, don\'t add\n if (materialIndicesByName[materialName] > 0) {\n unpacked.indices.push([]);\n }\n }\n // keep track of the current material index\n currentMaterialIndex = materialIndicesByName[materialName];\n // update current index array\n currentObjectByMaterialIndex = currentMaterialIndex;\n }\n else if (FACE_RE.test(line)) {\n // if this is a face\n /*\n split this face into an array of Vertex groups\n for example:\n f 16/92/11 14/101/22 1/69/1\n becomes:\n [\'16/92/11\', \'14/101/22\', \'1/69/1\'];\n */\n const triangles = triangulate(elements);\n for (const triangle of triangles) {\n for (let j = 0, eleLen = triangle.length; j < eleLen; j++) {\n const hash = triangle[j] + "," + currentMaterialIndex;\n if (hash in unpacked.hashindices) {\n unpacked.indices[currentObjectByMaterialIndex].push(unpacked.hashindices[hash]);\n }\n else {\n /*\n Each element of the face line array is a Vertex which has its\n attributes delimited by a forward slash. This will separate\n each attribute into another array:\n \'19/92/11\'\n becomes:\n Vertex = [\'19\', \'92\', \'11\'];\n where\n Vertex[0] is the vertex index\n Vertex[1] is the texture index\n Vertex[2] is the normal index\n Think of faces having Vertices which are comprised of the\n attributes location (v), texture (vt), and normal (vn).\n */\n const vertex = triangle[j].split("/");\n // it\'s possible for faces to only specify the vertex\n // and the normal. In this case, vertex will only have\n // a length of 2 and not 3 and the normal will be the\n // second item in the list with an index of 1.\n const normalIndex = vertex.length - 1;\n /*\n The verts, textures, and vertNormals arrays each contain a\n flattend array of coordinates.\n\n Because it gets confusing by referring to Vertex and then\n vertex (both are different in my descriptions) I will explain\n what\'s going on using the vertexNormals array:\n\n vertex[2] will contain the one-based index of the vertexNormals\n section (vn). One is subtracted from this index number to play\n nice with javascript\'s zero-based array indexing.\n\n Because vertexNormal is a flattened array of x, y, z values,\n simple pointer arithmetic is used to skip to the start of the\n vertexNormal, then the offset is added to get the correct\n component: +0 is x, +1 is y, +2 is z.\n\n This same process is repeated for verts and textures.\n */\n // Vertex position\n unpacked.verts.push(+verts[(+vertex[0] - 1) * 3 + 0]);\n unpacked.verts.push(+verts[(+vertex[0] - 1) * 3 + 1]);\n unpacked.verts.push(+verts[(+vertex[0] - 1) * 3 + 2]);\n // Vertex textures\n if (textures.length) {\n const stride = options.enableWTextureCoord ? 3 : 2;\n unpacked.textures.push(+textures[(+vertex[1] - 1) * stride + 0]);\n unpacked.textures.push(+textures[(+vertex[1] - 1) * stride + 1]);\n if (options.enableWTextureCoord) {\n unpacked.textures.push(+textures[(+vertex[1] - 1) * stride + 2]);\n }\n }\n // Vertex normals\n unpacked.norms.push(+vertNormals[(+vertex[normalIndex] - 1) * 3 + 0]);\n unpacked.norms.push(+vertNormals[(+vertex[normalIndex] - 1) * 3 + 1]);\n unpacked.norms.push(+vertNormals[(+vertex[normalIndex] - 1) * 3 + 2]);\n // Vertex material indices\n unpacked.materialIndices.push(currentMaterialIndex);\n // add the newly created Vertex to the list of indices\n unpacked.hashindices[hash] = unpacked.index;\n unpacked.indices[currentObjectByMaterialIndex].push(unpacked.hashindices[hash]);\n // increment the counter\n unpacked.index += 1;\n }\n }\n }\n }\n }\n this.vertices = unpacked.verts;\n this.vertexNormals = unpacked.norms;\n this.textures = unpacked.textures;\n this.vertexMaterialIndices = unpacked.materialIndices;\n this.indices = unpacked.indices[currentObjectByMaterialIndex];\n this.indicesPerMaterial = unpacked.indices;\n this.materialNames = materialNamesByIndex;\n this.materialIndices = materialIndicesByName;\n this.materialsByIndex = {};\n if (options.calcTangentsAndBitangents) {\n this.calculateTangentsAndBitangents();\n }\n }\n /**\n * Calculates the tangents and bitangents of the mesh that forms an orthogonal basis together with the\n * normal in the direction of the texture coordinates. These are useful for setting up the TBN matrix\n * when distorting the normals through normal maps.\n * Method derived from: http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/\n *\n * This method requires the normals and texture coordinates to be parsed and set up correctly.\n * Adds the tangents and bitangents as members of the class instance.\n */\n calculateTangentsAndBitangents() {\n console.assert(!!(this.vertices &&\n this.vertices.length &&\n this.vertexNormals &&\n this.vertexNormals.length &&\n this.textures &&\n this.textures.length), "Missing attributes for calculating tangents and bitangents");\n const unpacked = {\n tangents: [...new Array(this.vertices.length)].map(_ => 0),\n bitangents: [...new Array(this.vertices.length)].map(_ => 0),\n };\n // Loop through all faces in the whole mesh\n const indices = this.indices;\n const vertices = this.vertices;\n const normals = this.vertexNormals;\n const uvs = this.textures;\n for (let i = 0; i < indices.length; i += 3) {\n const i0 = indices[i + 0];\n const i1 = indices[i + 1];\n const i2 = indices[i + 2];\n const x_v0 = vertices[i0 * 3 + 0];\n const y_v0 = vertices[i0 * 3 + 1];\n const z_v0 = vertices[i0 * 3 + 2];\n const x_uv0 = uvs[i0 * 2 + 0];\n const y_uv0 = uvs[i0 * 2 + 1];\n const x_v1 = vertices[i1 * 3 + 0];\n const y_v1 = vertices[i1 * 3 + 1];\n const z_v1 = vertices[i1 * 3 + 2];\n const x_uv1 = uvs[i1 * 2 + 0];\n const y_uv1 = uvs[i1 * 2 + 1];\n const x_v2 = vertices[i2 * 3 + 0];\n const y_v2 = vertices[i2 * 3 + 1];\n const z_v2 = vertices[i2 * 3 + 2];\n const x_uv2 = uvs[i2 * 2 + 0];\n const y_uv2 = uvs[i2 * 2 + 1];\n const x_deltaPos1 = x_v1 - x_v0;\n const y_deltaPos1 = y_v1 - y_v0;\n const z_deltaPos1 = z_v1 - z_v0;\n const x_deltaPos2 = x_v2 - x_v0;\n const y_deltaPos2 = y_v2 - y_v0;\n const z_deltaPos2 = z_v2 - z_v0;\n const x_uvDeltaPos1 = x_uv1 - x_uv0;\n const y_uvDeltaPos1 = y_uv1 - y_uv0;\n const x_uvDeltaPos2 = x_uv2 - x_uv0;\n const y_uvDeltaPos2 = y_uv2 - y_uv0;\n const rInv = x_uvDeltaPos1 * y_uvDeltaPos2 - y_uvDeltaPos1 * x_uvDeltaPos2;\n const r = 1.0 / Math.abs(rInv < 0.0001 ? 1.0 : rInv);\n // Tangent\n const x_tangent = (x_deltaPos1 * y_uvDeltaPos2 - x_deltaPos2 * y_uvDeltaPos1) * r;\n const y_tangent = (y_deltaPos1 * y_uvDeltaPos2 - y_deltaPos2 * y_uvDeltaPos1) * r;\n const z_tangent = (z_deltaPos1 * y_uvDeltaPos2 - z_deltaPos2 * y_uvDeltaPos1) * r;\n // Bitangent\n const x_bitangent = (x_deltaPos2 * x_uvDeltaPos1 - x_deltaPos1 * x_uvDeltaPos2) * r;\n const y_bitangent = (y_deltaPos2 * x_uvDeltaPos1 - y_deltaPos1 * x_uvDeltaPos2) * r;\n const z_bitangent = (z_deltaPos2 * x_uvDeltaPos1 - z_deltaPos1 * x_uvDeltaPos2) * r;\n // Gram-Schmidt orthogonalize\n //t = glm::normalize(t - n * glm:: dot(n, t));\n const x_n0 = normals[i0 * 3 + 0];\n const y_n0 = normals[i0 * 3 + 1];\n const z_n0 = normals[i0 * 3 + 2];\n const x_n1 = normals[i1 * 3 + 0];\n const y_n1 = normals[i1 * 3 + 1];\n const z_n1 = normals[i1 * 3 + 2];\n const x_n2 = normals[i2 * 3 + 0];\n const y_n2 = normals[i2 * 3 + 1];\n const z_n2 = normals[i2 * 3 + 2];\n // Tangent\n const n0_dot_t = x_tangent * x_n0 + y_tangent * y_n0 + z_tangent * z_n0;\n const n1_dot_t = x_tangent * x_n1 + y_tangent * y_n1 + z_tangent * z_n1;\n const n2_dot_t = x_tangent * x_n2 + y_tangent * y_n2 + z_tangent * z_n2;\n const x_resTangent0 = x_tangent - x_n0 * n0_dot_t;\n const y_resTangent0 = y_tangent - y_n0 * n0_dot_t;\n const z_resTangent0 = z_tangent - z_n0 * n0_dot_t;\n const x_resTangent1 = x_tangent - x_n1 * n1_dot_t;\n const y_resTangent1 = y_tangent - y_n1 * n1_dot_t;\n const z_resTangent1 = z_tangent - z_n1 * n1_dot_t;\n const x_resTangent2 = x_tangent - x_n2 * n2_dot_t;\n const y_resTangent2 = y_tangent - y_n2 * n2_dot_t;\n const z_resTangent2 = z_tangent - z_n2 * n2_dot_t;\n const magTangent0 = Math.sqrt(x_resTangent0 * x_resTangent0 + y_resTangent0 * y_resTangent0 + z_resTangent0 * z_resTangent0);\n const magTangent1 = Math.sqrt(x_resTangent1 * x_resTangent1 + y_resTangent1 * y_resTangent1 + z_resTangent1 * z_resTangent1);\n const magTangent2 = Math.sqrt(x_resTangent2 * x_resTangent2 + y_resTangent2 * y_resTangent2 + z_resTangent2 * z_resTangent2);\n // Bitangent\n const n0_dot_bt = x_bitangent * x_n0 + y_bitangent * y_n0 + z_bitangent * z_n0;\n const n1_dot_bt = x_bitangent * x_n1 + y_bitangent * y_n1 + z_bitangent * z_n1;\n const n2_dot_bt = x_bitangent * x_n2 + y_bitangent * y_n2 + z_bitangent * z_n2;\n const x_resBitangent0 = x_bitangent - x_n0 * n0_dot_bt;\n const y_resBitangent0 = y_bitangent - y_n0 * n0_dot_bt;\n const z_resBitangent0 = z_bitangent - z_n0 * n0_dot_bt;\n const x_resBitangent1 = x_bitangent - x_n1 * n1_dot_bt;\n const y_resBitangent1 = y_bitangent - y_n1 * n1_dot_bt;\n const z_resBitangent1 = z_bitangent - z_n1 * n1_dot_bt;\n const x_resBitangent2 = x_bitangent - x_n2 * n2_dot_bt;\n const y_resBitangent2 = y_bitangent - y_n2 * n2_dot_bt;\n const z_resBitangent2 = z_bitangent - z_n2 * n2_dot_bt;\n const magBitangent0 = Math.sqrt(x_resBitangent0 * x_resBitangent0 +\n y_resBitangent0 * y_resBitangent0 +\n z_resBitangent0 * z_resBitangent0);\n const magBitangent1 = Math.sqrt(x_resBitangent1 * x_resBitangent1 +\n y_resBitangent1 * y_resBitangent1 +\n z_resBitangent1 * z_resBitangent1);\n const magBitangent2 = Math.sqrt(x_resBitangent2 * x_resBitangent2 +\n y_resBitangent2 * y_resBitangent2 +\n z_resBitangent2 * z_resBitangent2);\n unpacked.tangents[i0 * 3 + 0] += x_resTangent0 / magTangent0;\n unpacked.tangents[i0 * 3 + 1] += y_resTangent0 / magTangent0;\n unpacked.tangents[i0 * 3 + 2] += z_resTangent0 / magTangent0;\n unpacked.tangents[i1 * 3 + 0] += x_resTangent1 / magTangent1;\n unpacked.tangents[i1 * 3 + 1] += y_resTangent1 / magTangent1;\n unpacked.tangents[i1 * 3 + 2] += z_resTangent1 / magTangent1;\n unpacked.tangents[i2 * 3 + 0] += x_resTangent2 / magTangent2;\n unpacked.tangents[i2 * 3 + 1] += y_resTangent2 / magTangent2;\n unpacked.tangents[i2 * 3 + 2] += z_resTangent2 / magTangent2;\n unpacked.bitangents[i0 * 3 + 0] += x_resBitangent0 / magBitangent0;\n unpacked.bitangents[i0 * 3 + 1] += y_resBitangent0 / magBitangent0;\n unpacked.bitangents[i0 * 3 + 2] += z_resBitangent0 / magBitangent0;\n unpacked.bitangents[i1 * 3 + 0] += x_resBitangent1 / magBitangent1;\n unpacked.bitangents[i1 * 3 + 1] += y_resBitangent1 / magBitangent1;\n unpacked.bitangents[i1 * 3 + 2] += z_resBitangent1 / magBitangent1;\n unpacked.bitangents[i2 * 3 + 0] += x_resBitangent2 / magBitangent2;\n unpacked.bitangents[i2 * 3 + 1] += y_resBitangent2 / magBitangent2;\n unpacked.bitangents[i2 * 3 + 2] += z_resBitangent2 / magBitangent2;\n // TODO: check handedness\n }\n this.tangents = unpacked.tangents;\n this.bitangents = unpacked.bitangents;\n }\n /**\n * @param layout - A {@link Layout} object that describes the\n * desired memory layout of the generated buffer\n * @return The packed array in the ... TODO\n */\n makeBufferData(layout) {\n const numItems = this.vertices.length / 3;\n const buffer = new ArrayBuffer(layout.stride * numItems);\n buffer.numItems = numItems;\n const dataView = new DataView(buffer);\n for (let i = 0, vertexOffset = 0; i < numItems; i++) {\n vertexOffset = i * layout.stride;\n // copy in the vertex data in the order and format given by the\n // layout param\n for (const attribute of layout.attributes) {\n const offset = vertexOffset + layout.attributeMap[attribute.key].offset;\n switch (attribute.key) {\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].POSITION.key:\n dataView.setFloat32(offset, this.vertices[i * 3], true);\n dataView.setFloat32(offset + 4, this.vertices[i * 3 + 1], true);\n dataView.setFloat32(offset + 8, this.vertices[i * 3 + 2], true);\n break;\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].UV.key:\n dataView.setFloat32(offset, this.textures[i * 2], true);\n dataView.setFloat32(offset + 4, this.textures[i * 2 + 1], true);\n break;\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].NORMAL.key:\n dataView.setFloat32(offset, this.vertexNormals[i * 3], true);\n dataView.setFloat32(offset + 4, this.vertexNormals[i * 3 + 1], true);\n dataView.setFloat32(offset + 8, this.vertexNormals[i * 3 + 2], true);\n break;\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].MATERIAL_INDEX.key:\n dataView.setInt16(offset, this.vertexMaterialIndices[i], true);\n break;\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].AMBIENT.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.ambient[0], true);\n dataView.setFloat32(offset + 4, material.ambient[1], true);\n dataView.setFloat32(offset + 8, material.ambient[2], true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].DIFFUSE.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.diffuse[0], true);\n dataView.setFloat32(offset + 4, material.diffuse[1], true);\n dataView.setFloat32(offset + 8, material.diffuse[2], true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].SPECULAR.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.specular[0], true);\n dataView.setFloat32(offset + 4, material.specular[1], true);\n dataView.setFloat32(offset + 8, material.specular[2], true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].SPECULAR_EXPONENT.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.specularExponent, true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].EMISSIVE.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.emissive[0], true);\n dataView.setFloat32(offset + 4, material.emissive[1], true);\n dataView.setFloat32(offset + 8, material.emissive[2], true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].TRANSMISSION_FILTER.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.transmissionFilter[0], true);\n dataView.setFloat32(offset + 4, material.transmissionFilter[1], true);\n dataView.setFloat32(offset + 8, material.transmissionFilter[2], true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].DISSOLVE.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.dissolve, true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].ILLUMINATION.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setInt16(offset, material.illumination, true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].REFRACTION_INDEX.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.refractionIndex, true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].SHARPNESS.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setFloat32(offset, material.sharpness, true);\n break;\n }\n case _layout__WEBPACK_IMPORTED_MODULE_0__["Layout"].ANTI_ALIASING.key: {\n const materialIndex = this.vertexMaterialIndices[i];\n const material = this.materialsByIndex[materialIndex];\n if (!material) {\n console.warn(\'Material "\' +\n this.materialNames[materialIndex] +\n \'" not found in mesh. Did you forget to call addMaterialLibrary(...)?"\');\n break;\n }\n dataView.setInt16(offset, material.antiAliasing ? 1 : 0, true);\n break;\n }\n }\n }\n }\n return buffer;\n }\n makeIndexBufferData() {\n const buffer = new Uint16Array(this.indices);\n buffer.numItems = this.indices.length;\n return buffer;\n }\n makeIndexBufferDataForMaterials(...materialIndices) {\n const indices = new Array().concat(...materialIndices.map(mtlIdx => this.indicesPerMaterial[mtlIdx]));\n const buffer = new Uint16Array(indices);\n buffer.numItems = indices.length;\n return buffer;\n }\n addMaterialLibrary(mtl) {\n for (const name in mtl.materials) {\n if (!(name in this.materialIndices)) {\n // This material is not referenced by the mesh\n continue;\n }\n const material = mtl.materials[name];\n // Find the material index for this material\n const materialIndex = this.materialIndices[material.name];\n // Put the material into the materialsByIndex object at the right\n // spot as determined when the obj file was parsed\n this.materialsByIndex[materialIndex] = material;\n }\n }\n}\nfunction* triangulate(elements) {\n if (elements.length <= 3) {\n yield elements;\n }\n else if (elements.length === 4) {\n yield [elements[0], elements[1], elements[2]];\n yield [elements[2], elements[3], elements[0]];\n }\n else {\n for (let i = 1; i < elements.length - 1; i++) {\n yield [elements[0], elements[i], elements[i + 1]];\n }\n }\n}\n\n\n//# sourceURL=webpack:///./src/mesh.ts?');},"./src/utils.ts":
1881
- /*!**********************!*\
1882
- !*** ./src/utils.ts ***!
1883
- \**********************/
1884
- /*! exports provided: downloadModels, downloadMeshes, initMeshBuffers, deleteMeshBuffers */function(module,__webpack_exports__,__webpack_require__){eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "downloadModels", function() { return downloadModels; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "downloadMeshes", function() { return downloadMeshes; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "initMeshBuffers", function() { return initMeshBuffers; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "deleteMeshBuffers", function() { return deleteMeshBuffers; });\n/* harmony import */ var _mesh__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./mesh */ "./src/mesh.ts");\n/* harmony import */ var _material__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./material */ "./src/material.ts");\n\n\nfunction downloadMtlTextures(mtl, root) {\n const mapAttributes = [\n "mapDiffuse",\n "mapAmbient",\n "mapSpecular",\n "mapDissolve",\n "mapBump",\n "mapDisplacement",\n "mapDecal",\n "mapEmissive",\n ];\n if (!root.endsWith("/")) {\n root += "/";\n }\n const textures = [];\n for (const materialName in mtl.materials) {\n if (!mtl.materials.hasOwnProperty(materialName)) {\n continue;\n }\n const material = mtl.materials[materialName];\n for (const attr of mapAttributes) {\n const mapData = material[attr];\n if (!mapData || !mapData.filename) {\n continue;\n }\n const url = root + mapData.filename;\n textures.push(fetch(url)\n .then(response => {\n if (!response.ok) {\n throw new Error();\n }\n return response.blob();\n })\n .then(function (data) {\n const image = new Image();\n image.src = URL.createObjectURL(data);\n mapData.texture = image;\n return new Promise(resolve => (image.onload = resolve));\n })\n .catch(() => {\n console.error(`Unable to download texture: ${url}`);\n }));\n }\n }\n return Promise.all(textures);\n}\nfunction getMtl(modelOptions) {\n if (!(typeof modelOptions.mtl === "string")) {\n return modelOptions.obj.replace(/\\.obj$/, ".mtl");\n }\n return modelOptions.mtl;\n}\n/**\n * Accepts a list of model request objects and returns a Promise that\n * resolves when all models have been downloaded and parsed.\n *\n * The list of model objects follow this interface:\n * {\n * obj: \'path/to/model.obj\',\n * mtl: true | \'path/to/model.mtl\',\n * downloadMtlTextures: true | false\n * mtlTextureRoot: \'/models/suzanne/maps\'\n * name: \'suzanne\'\n * }\n *\n * The `obj` attribute is required and should be the path to the\n * model\'s .obj file relative to the current repo (absolute URLs are\n * suggested).\n *\n * The `mtl` attribute is optional and can either be a boolean or\n * a path to the model\'s .mtl file relative to the current URL. If\n * the value is `true`, then the path and basename given for the `obj`\n * attribute is used replacing the .obj suffix for .mtl\n * E.g.: {obj: \'models/foo.obj\', mtl: true} would search for \'models/foo.mtl\'\n *\n * The `name` attribute is optional and is a human friendly name to be\n * included with the parsed OBJ and MTL files. If not given, the base .obj\n * filename will be used.\n *\n * The `downloadMtlTextures` attribute is a flag for automatically downloading\n * any images found in the MTL file and attaching them to each Material\n * created from that file. For example, if material.mapDiffuse is set (there\n * was data in the MTL file), then material.mapDiffuse.texture will contain\n * the downloaded image. This option defaults to `true`. By default, the MTL\'s\n * URL will be used to determine the location of the images.\n *\n * The `mtlTextureRoot` attribute is optional and should point to the location\n * on the server that this MTL\'s texture files are located. The default is to\n * use the MTL file\'s location.\n *\n * @returns {Promise} the result of downloading the given list of models. The\n * promise will resolve with an object whose keys are the names of the models\n * and the value is its Mesh object. Each Mesh object will automatically\n * have its addMaterialLibrary() method called to set the given MTL data (if given).\n */\nfunction downloadModels(models) {\n const finished = [];\n for (const model of models) {\n if (!model.obj) {\n throw new Error(\'"obj" attribute of model object not set. The .obj file is required to be set \' +\n "in order to use downloadModels()");\n }\n const options = {\n indicesPerMaterial: !!model.indicesPerMaterial,\n calcTangentsAndBitangents: !!model.calcTangentsAndBitangents,\n };\n // if the name is not provided, dervive it from the given OBJ\n let name = model.name;\n if (!name) {\n const parts = model.obj.split("/");\n name = parts[parts.length - 1].replace(".obj", "");\n }\n const namePromise = Promise.resolve(name);\n const meshPromise = fetch(model.obj)\n .then(response => response.text())\n .then(data => {\n return new _mesh__WEBPACK_IMPORTED_MODULE_0__["default"](data, options);\n });\n let mtlPromise;\n // Download MaterialLibrary file?\n if (model.mtl) {\n const mtl = getMtl(model);\n mtlPromise = fetch(mtl)\n .then(response => response.text())\n .then((data) => {\n const material = new _material__WEBPACK_IMPORTED_MODULE_1__["MaterialLibrary"](data);\n if (model.downloadMtlTextures !== false) {\n let root = model.mtlTextureRoot;\n if (!root) {\n // get the directory of the MTL file as default\n root = mtl.substr(0, mtl.lastIndexOf("/"));\n }\n // downloadMtlTextures returns a Promise that\n // is resolved once all of the images it\n // contains are downloaded. These are then\n // attached to the map data objects\n return Promise.all([Promise.resolve(material), downloadMtlTextures(material, root)]);\n }\n return Promise.all([Promise.resolve(material), undefined]);\n })\n .then((value) => {\n return value[0];\n });\n }\n const parsed = [namePromise, meshPromise, mtlPromise];\n finished.push(Promise.all(parsed));\n }\n return Promise.all(finished).then(ms => {\n // the "finished" promise is a list of name, Mesh instance,\n // and MaterialLibary instance. This unpacks and returns an\n // object mapping name to Mesh (Mesh points to MTL).\n const models = {};\n for (const model of ms) {\n const [name, mesh, mtl] = model;\n mesh.name = name;\n if (mtl) {\n mesh.addMaterialLibrary(mtl);\n }\n models[name] = mesh;\n }\n return models;\n });\n}\n/**\n * Takes in an object of `mesh_name`, `\'/url/to/OBJ/file\'` pairs and a callback\n * function. Each OBJ file will be ajaxed in and automatically converted to\n * an OBJ.Mesh. When all files have successfully downloaded the callback\n * function provided will be called and passed in an object containing\n * the newly created meshes.\n *\n * **Note:** In order to use this function as a way to download meshes, a\n * webserver of some sort must be used.\n *\n * @param {Object} nameAndAttrs an object where the key is the name of the mesh and the value is the url to that mesh\'s OBJ file\n *\n * @param {Function} completionCallback should contain a function that will take one parameter: an object array where the keys will be the unique object name and the value will be a Mesh object\n *\n * @param {Object} meshes In case other meshes are loaded separately or if a previously declared variable is desired to be used, pass in a (possibly empty) json object of the pattern: { \'<mesh_name>\': OBJ.Mesh }\n *\n */\nfunction downloadMeshes(nameAndURLs, completionCallback, meshes) {\n if (meshes === undefined) {\n meshes = {};\n }\n const completed = [];\n for (const mesh_name in nameAndURLs) {\n if (!nameAndURLs.hasOwnProperty(mesh_name)) {\n continue;\n }\n const url = nameAndURLs[mesh_name];\n completed.push(fetch(url)\n .then(response => response.text())\n .then(data => {\n return [mesh_name, new _mesh__WEBPACK_IMPORTED_MODULE_0__["default"](data)];\n }));\n }\n Promise.all(completed).then(ms => {\n for (const [name, mesh] of ms) {\n meshes[name] = mesh;\n }\n return completionCallback(meshes);\n });\n}\nfunction _buildBuffer(gl, type, data, itemSize) {\n const buffer = gl.createBuffer();\n const arrayView = type === gl.ARRAY_BUFFER ? Float32Array : Uint16Array;\n gl.bindBuffer(type, buffer);\n gl.bufferData(type, new arrayView(data), gl.STATIC_DRAW);\n buffer.itemSize = itemSize;\n buffer.numItems = data.length / itemSize;\n return buffer;\n}\n/**\n * Takes in the WebGL context and a Mesh, then creates and appends the buffers\n * to the mesh object as attributes.\n *\n * @param {WebGLRenderingContext} gl the `canvas.getContext(\'webgl\')` context instance\n * @param {Mesh} mesh a single `OBJ.Mesh` instance\n *\n * The newly created mesh attributes are:\n *\n * Attrbute | Description\n * :--- | ---\n * **normalBuffer** |contains the model&#39;s Vertex Normals\n * normalBuffer.itemSize |set to 3 items\n * normalBuffer.numItems |the total number of vertex normals\n * |\n * **textureBuffer** |contains the model&#39;s Texture Coordinates\n * textureBuffer.itemSize |set to 2 items\n * textureBuffer.numItems |the number of texture coordinates\n * |\n * **vertexBuffer** |contains the model&#39;s Vertex Position Coordinates (does not include w)\n * vertexBuffer.itemSize |set to 3 items\n * vertexBuffer.numItems |the total number of vertices\n * |\n * **indexBuffer** |contains the indices of the faces\n * indexBuffer.itemSize |is set to 1\n * indexBuffer.numItems |the total number of indices\n *\n * A simple example (a lot of steps are missing, so don\'t copy and paste):\n *\n * const gl = canvas.getContext(\'webgl\'),\n * mesh = OBJ.Mesh(obj_file_data);\n * // compile the shaders and create a shader program\n * const shaderProgram = gl.createProgram();\n * // compilation stuff here\n * ...\n * // make sure you have vertex, vertex normal, and texture coordinate\n * // attributes located in your shaders and attach them to the shader program\n * shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");\n * gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);\n *\n * shaderProgram.vertexNormalAttribute = gl.getAttribLocation(shaderProgram, "aVertexNormal");\n * gl.enableVertexAttribArray(shaderProgram.vertexNormalAttribute);\n *\n * shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord");\n * gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute);\n *\n * // create and initialize the vertex, vertex normal, and texture coordinate buffers\n * // and save on to the mesh object\n * OBJ.initMeshBuffers(gl, mesh);\n *\n * // now to render the mesh\n * gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vertexBuffer);\n * gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, mesh.vertexBuffer.itemSize, gl.FLOAT, false, 0, 0);\n * // it\'s possible that the mesh doesn\'t contain\n * // any texture coordinates (e.g. suzanne.obj in the development branch).\n * // in this case, the texture vertexAttribArray will need to be disabled\n * // before the call to drawElements\n * if(!mesh.textures.length){\n * gl.disableVertexAttribArray(shaderProgram.textureCoordAttribute);\n * }\n * else{\n * // if the texture vertexAttribArray has been previously\n * // disabled, then it needs to be re-enabled\n * gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute);\n * gl.bindBuffer(gl.ARRAY_BUFFER, mesh.textureBuffer);\n * gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, mesh.textureBuffer.itemSize, gl.FLOAT, false, 0, 0);\n * }\n *\n * gl.bindBuffer(gl.ARRAY_BUFFER, mesh.normalBuffer);\n * gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, mesh.normalBuffer.itemSize, gl.FLOAT, false, 0, 0);\n *\n * gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.mesh.indexBuffer);\n * gl.drawElements(gl.TRIANGLES, model.mesh.indexBuffer.numItems, gl.UNSIGNED_SHORT, 0);\n */\nfunction initMeshBuffers(gl, mesh) {\n mesh.normalBuffer = _buildBuffer(gl, gl.ARRAY_BUFFER, mesh.vertexNormals, 3);\n mesh.textureBuffer = _buildBuffer(gl, gl.ARRAY_BUFFER, mesh.textures, mesh.textureStride);\n mesh.vertexBuffer = _buildBuffer(gl, gl.ARRAY_BUFFER, mesh.vertices, 3);\n mesh.indexBuffer = _buildBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, mesh.indices, 1);\n return mesh;\n}\nfunction deleteMeshBuffers(gl, mesh) {\n gl.deleteBuffer(mesh.normalBuffer);\n gl.deleteBuffer(mesh.textureBuffer);\n gl.deleteBuffer(mesh.vertexBuffer);\n gl.deleteBuffer(mesh.indexBuffer);\n}\n\n\n//# sourceURL=webpack:///./src/utils.ts?');},0:
1885
- /*!****************************!*\
1886
- !*** multi ./src/index.ts ***!
1887
- \****************************/
1888
- /*! no static exports found */function(module,exports,__webpack_require__){eval('module.exports = __webpack_require__(/*! /home/aaron/google_drive/projects/webgl-obj-loader/src/index.ts */"./src/index.ts");\n\n\n//# sourceURL=webpack:///multi_./src/index.ts?');}})}));
1889
- } (webglObjLoader_min$1));
1890
- return webglObjLoader_min$1.exports;
1891
- }
1892
1862
 
1893
- var webglObjLoader_minExports = requireWebglObjLoader_min();
1894
- var OBJ = /*@__PURE__*/getDefaultExportFromCjs(webglObjLoader_minExports);
1863
+ const _index = (s, count) => {
1864
+ const i = parseInt(s, 10);
1865
+ return i < 0 ? count + i : i - 1;
1866
+ };
1867
+
1868
+ /**
1869
+ * Parse OBJ text into `{ position, normal?, texcoord?, indices }`; normal and
1870
+ * texcoord only when some face vertex names one.
1871
+ * @param {string} text
1872
+ * @returns {{ position:{numComponents:number,data:Float32Array},
1873
+ * normal?:{numComponents:number,data:Float32Array},
1874
+ * texcoord?:{numComponents:number,data:Float32Array},
1875
+ * indices:{numComponents:number,data:Uint32Array} }}
1876
+ */
1877
+ function parseObj(text) {
1878
+ const v = [], vt = [], vn = [];
1879
+ const position = [], texcoord = [], normal = [], indices = [];
1880
+ const seen = new Map();
1881
+ let uvs = false, normals = false;
1882
+
1883
+ const vertex = (token) => {
1884
+ const [ps, ts, ns] = token.split('/');
1885
+ const p = _index(ps, v.length / 3);
1886
+ const t = ts ? _index(ts, vt.length / 2) : -1;
1887
+ const n = ns ? _index(ns, vn.length / 3) : -1;
1888
+ const key = p + '/' + t + '/' + n;
1889
+ let id = seen.get(key);
1890
+ if (id !== undefined) return id;
1891
+ position.push(v[3 * p], v[3 * p + 1], v[3 * p + 2]);
1892
+ if (t >= 0) { texcoord.push(vt[2 * t], vt[2 * t + 1]); uvs = true; } else texcoord.push(0, 0);
1893
+ if (n >= 0) { normal.push(vn[3 * n], vn[3 * n + 1], vn[3 * n + 2]); normals = true; } else normal.push(0, 0, 0);
1894
+ id = position.length / 3 - 1;
1895
+ seen.set(key, id);
1896
+ return id;
1897
+ };
1898
+
1899
+ for (const raw of text.split('\n')) {
1900
+ const line = raw.trim();
1901
+ if (!line || line[0] === '#') continue;
1902
+ const parts = line.split(/\s+/);
1903
+ switch (parts[0]) {
1904
+ case 'v': v.push(+parts[1], +parts[2], +parts[3]); break;
1905
+ case 'vt': vt.push(+parts[1], +(parts[2] ?? 0)); break;
1906
+ case 'vn': vn.push(+parts[1], +parts[2], +parts[3]); break;
1907
+ case 'f': {
1908
+ const ids = parts.slice(1).map(vertex);
1909
+ if (ids.length === 4) indices.push(ids[0], ids[1], ids[2], ids[2], ids[3], ids[0]);
1910
+ else for (let i = 1; i + 1 < ids.length; i++) indices.push(ids[0], ids[i], ids[i + 1]);
1911
+ break;
1912
+ }
1913
+ }
1914
+ }
1915
+
1916
+ const out = {
1917
+ position: { numComponents: 3, data: new Float32Array(position) },
1918
+ indices: { numComponents: 3, data: new Uint32Array(indices) },
1919
+ };
1920
+ if (normals) out.normal = { numComponents: 3, data: new Float32Array(normal) };
1921
+ if (uvs) out.texcoord = { numComponents: 2, data: new Float32Array(texcoord) };
1922
+ return out;
1923
+ }
1895
1924
 
1896
1925
  /**
1897
1926
  * @file Media sources — images, video and Canvas2D rasters for a bridge's textures, OBJ models for its buffers.
@@ -1899,7 +1928,7 @@ var OBJ = /*@__PURE__*/getDefaultExportFromCjs(webglObjLoader_minExports);
1899
1928
  * @license AGPL-3.0-only
1900
1929
  *
1901
1930
  * A model arrives as arrays for a bridge's buffer upload: loadModel fetches an
1902
- * OBJ file and adapts webgl-obj-loader's mesh, no parser of its own.
1931
+ * OBJ file and parses it (host/obj).
1903
1932
  *
1904
1933
  * What a texture upload reads from: an ImageBitmap fetched from a URL, a
1905
1934
  * hidden <video> element from a file or the user's camera, a bitmap drawn
@@ -1931,8 +1960,8 @@ async function loadImage(url, opts) {
1931
1960
  * and texcoord when the file carries them. twgl's createBufferInfoFromArrays
1932
1961
  * takes the result as it is.
1933
1962
  *
1934
- * Parsing is webgl-obj-loader's: faces are triangulated and each distinct
1935
- * position / uv / normal triple becomes one vertex. Materials are ignored.
1963
+ * Faces are triangulated and each distinct position / uv / normal triple
1964
+ * becomes one vertex. Materials are ignored.
1936
1965
  *
1937
1966
  * @param {string} url
1938
1967
  * @param {{ fetch?:object }} [opts] fetch: the fetch init (credentials, mode, …).
@@ -1945,16 +1974,7 @@ async function loadModel(url, opts) {
1945
1974
  const o = opts || {};
1946
1975
  const res = await fetch(url, o.fetch);
1947
1976
  if (!res.ok) throw new Error('[host] model: ' + url + ' → ' + res.status);
1948
- const mesh = new OBJ.Mesh(await res.text());
1949
- const out = {
1950
- position: { numComponents: 3, data: new Float32Array(mesh.vertices) },
1951
- indices: { numComponents: 3, data: new Uint32Array(mesh.indices) },
1952
- };
1953
- // a file without normals leaves the loader's normals unset, one per component
1954
- const n = mesh.vertexNormals;
1955
- if (n.length && n.every(Number.isFinite)) out.normal = { numComponents: 3, data: new Float32Array(n) };
1956
- if (mesh.textures.length) out.texcoord = { numComponents: mesh.textureStride, data: new Float32Array(mesh.textures) };
1957
- return out;
1977
+ return parseObj(await res.text());
1958
1978
  }
1959
1979
 
1960
1980
  /**
@@ -2265,9 +2285,14 @@ function createLabels(host, opts) {
2265
2285
  * Two-pointer motion is computed from the pointers' own coordinates, never
2266
2286
  * from a browser's pinch synthesis, so it reads the same on every platform;
2267
2287
  * the canvas gets touch-action: none while the orbit lives so the browser
2268
- * does not scroll or zoom the page instead. No damping, no inertia: direct
2269
- * manipulation wants exactness, and a sketch that wants smoothing lerps the
2270
- * camera it owns.
2288
+ * does not scroll or zoom the page instead. `damping`, seconds, makes the
2289
+ * camera trail the gesture through a first-order lag: each step of a drag —
2290
+ * orbit, pan and dolly lanes — and each wheel notch is an impulse the camera
2291
+ * drains with that time constant through tree's coast, so it follows the
2292
+ * finger about `damping` behind and, after a release, travels the rest of
2293
+ * what the finger did and no more, a flick easing out. A fresh touch drops
2294
+ * what is still draining, so do home() and enabled = false. The default is
2295
+ * 0.2 s; 0 makes the orbit exact.
2271
2296
  *
2272
2297
  * Screen y runs down; which world direction that is depends on the
2273
2298
  * projection installed in the view bag: a y-up projection (GL's, tree's
@@ -2278,19 +2303,23 @@ function createLabels(host, opts) {
2278
2303
 
2279
2304
 
2280
2305
  const _clamp = { min: 0, max: Infinity };
2306
+ const _now$1 = () => (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
2307
+ const EPS = 1e-4; // a rate below this in every lane ends the drain
2308
+ const DT_MAX = 0.1; // seconds, the measured frame's clamp
2281
2309
 
2282
2310
  /**
2283
2311
  * Create an orbit on a camera state.
2284
2312
  *
2285
2313
  * @param {object} host
2286
2314
  * @param {object} cam The camera state written.
2287
- * @param {{ rotate?:number, pan?:number, zoom?:number, wheel?:number,
2315
+ * @param {{ rotate?:number, pan?:number, zoom?:number, wheel?:number, damping?:number,
2288
2316
  * minDistance?:number, maxDistance?:number, enabled?:boolean }} [opts]
2289
2317
  * rotate: radians per pixel (default 0.005). pan / zoom / wheel:
2290
2318
  * scalars on the two-pointer pan, the pinch and the wheel dolly
2291
- * (default 1). minDistance / maxDistance: gaze-distance clamps.
2292
- * @returns {object} The orbit: { cam, enabled, rotate, pan, zoom, wheel,
2293
- * minDistance, maxDistance, update(), home(), dispose() }.
2319
+ * (default 1). damping: the lag's time constant in seconds (default
2320
+ * 0.2; 0 exact). minDistance / maxDistance: gaze-distance clamps.
2321
+ * @returns {object} The orbit: { cam, enabled, rotate, pan, zoom, wheel, damping,
2322
+ * minDistance, maxDistance, update(dt), home(), dispose() }.
2294
2323
  */
2295
2324
  function createOrbit(host, cam, opts) {
2296
2325
  const o = opts || {};
@@ -2298,6 +2327,16 @@ function createOrbit(host, cam, opts) {
2298
2327
  const home = cameraCopy(createCamera(), cam);
2299
2328
  const tracked = new Map(); // pointer id → { x, y } as last consumed
2300
2329
  let wheelAcc = 0;
2330
+ const rate = [0, 0, 0, 0, 0]; // what is still draining: dAz, dEl, panX, panY, log dolly — per second
2331
+ const step = [0, 0, 0, 0, 0]; // one frame of it
2332
+ let lastNow = 0, wasPressed = false;
2333
+
2334
+ const cancel = () => { for (let i = 0; i < 5; i++) rate[i] = 0; };
2335
+ const apply = (s) => {
2336
+ if (s[0] !== 0 || s[1] !== 0) cameraOrbit(cam, s[0], s[1]);
2337
+ if (s[2] !== 0 || s[3] !== 0) cameraPan(cam, s[2], s[3]);
2338
+ if (s[4] !== 0) dolly(Math.exp(s[4]));
2339
+ };
2301
2340
 
2302
2341
  const onWheel = (e) => {
2303
2342
  if (typeof e.preventDefault === 'function') e.preventDefault();
@@ -2335,16 +2374,23 @@ function createOrbit(host, cam, opts) {
2335
2374
  zoom: o.zoom ?? 1,
2336
2375
  /** Scalar on the wheel dolly. */
2337
2376
  wheel: o.wheel ?? 1,
2377
+ /** The lag's time constant, seconds; 0 makes the orbit exact. */
2378
+ damping: o.damping ?? 0.2,
2338
2379
  /** Gaze-distance clamps for every dolly. */
2339
2380
  minDistance: o.minDistance ?? 0,
2340
2381
  maxDistance: o.maxDistance ?? Infinity,
2341
2382
 
2342
2383
  /**
2343
- * Consume this frame's unclaimed pointer motion and wheel into cam.
2384
+ * Consume this frame's unclaimed pointer motion and wheel into cam —
2385
+ * through the lag when damping is on.
2386
+ * @param {number} [dt] The frame in seconds; measured since the last update when omitted.
2344
2387
  * @returns {boolean} Whether the camera moved.
2345
2388
  */
2346
- update() {
2347
- if (!orbit.enabled) { tracked.clear(); wheelAcc = 0; return false; }
2389
+ update(dt) {
2390
+ const now = _now$1();
2391
+ if (typeof dt !== 'number') dt = lastNow ? Math.min((now - lastNow) / 1000, DT_MAX) : 0;
2392
+ lastNow = now;
2393
+ if (!orbit.enabled) { tracked.clear(); wheelAcc = 0; cancel(); wasPressed = false; return false; }
2348
2394
  const src = host.pointer;
2349
2395
  let a = null, b = null;
2350
2396
  for (const p of src.pointers.values()) {
@@ -2356,14 +2402,19 @@ function createOrbit(host, cam, opts) {
2356
2402
  for (const id of tracked.keys()) {
2357
2403
  if ((a === null || id !== a.id) && (b === null || id !== b.id)) tracked.delete(id);
2358
2404
  }
2405
+ const tau = orbit.damping > 0 ? orbit.damping : 0;
2406
+ const pressed = a !== null;
2407
+ if (pressed && !wasPressed) cancel(); // a fresh touch catches the scene: what was still draining is dropped
2408
+ wasPressed = pressed;
2359
2409
  let moved = false;
2410
+ let az = 0, el = 0, px = 0, py = 0, ld = 0; // this frame's step
2360
2411
  const view = host.view;
2361
2412
  const ys = view.mat4Proj[5] < 0 ? -1 : 1; // screen-down is the eye's −up under a y-up projection, +up under p5's flip
2362
2413
  if (a !== null && b === null) {
2363
2414
  const t = tracked.get(a.id);
2364
2415
  if (t) {
2365
2416
  const dx = a.x - t.x, dy = a.y - t.y;
2366
- if (dx !== 0 || dy !== 0) { cameraOrbit(cam, -dx * orbit.rotate, ys * dy * orbit.rotate); moved = true; }
2417
+ if (dx !== 0 || dy !== 0) { az = -dx * orbit.rotate; el = ys * dy * orbit.rotate; }
2367
2418
  }
2368
2419
  track(a);
2369
2420
  } else if (a !== null && b !== null) {
@@ -2372,30 +2423,37 @@ function createOrbit(host, cam, opts) {
2372
2423
  const dmx = (a.x + b.x - ta.x - tb.x) / 2, dmy = (a.y + b.y - ta.y - tb.y) / 2;
2373
2424
  if (dmx !== 0 || dmy !== 0) {
2374
2425
  const ratio = pixelRatio(view.mat4Proj, -view.vp[3] || 1, -distance(), view.ndcZMin);
2375
- cameraPan(cam, -dmx * ratio * orbit.pan, ys * dmy * ratio * orbit.pan);
2376
- moved = true;
2426
+ px = -dmx * ratio * orbit.pan; py = ys * dmy * ratio * orbit.pan;
2377
2427
  }
2378
2428
  const d0 = Math.hypot(ta.x - tb.x, ta.y - tb.y), d1 = Math.hypot(a.x - b.x, a.y - b.y);
2379
- if (d0 > 0 && d1 > 0 && d0 !== d1) { dolly(Math.pow(d0 / d1, orbit.zoom)); moved = true; }
2429
+ if (d0 > 0 && d1 > 0 && d0 !== d1) ld = orbit.zoom * Math.log(d0 / d1);
2380
2430
  }
2381
2431
  track(a); track(b);
2382
2432
  }
2433
+ // the step: exact, or — with damping — an impulse the drain below spreads over time
2434
+ if (az !== 0 || el !== 0 || px !== 0 || py !== 0 || ld !== 0) {
2435
+ if (tau > 0) { rate[0] += az / tau; rate[1] += el / tau; rate[2] += px / tau; rate[3] += py / tau; rate[4] += ld / tau; }
2436
+ else { step[0] = az; step[1] = el; step[2] = px; step[3] = py; step[4] = ld; apply(step); moved = true; }
2437
+ }
2383
2438
  if (wheelAcc !== 0) {
2384
- dolly(Math.exp(wheelAcc * 0.001 * orbit.wheel));
2439
+ const w = wheelAcc * 0.001 * orbit.wheel;
2385
2440
  wheelAcc = 0;
2386
- moved = true;
2441
+ if (tau > 0) rate[4] += w / tau;
2442
+ else { dolly(Math.exp(w)); moved = true; }
2387
2443
  }
2444
+ if (tau > 0 && coastAlive(rate, EPS)) { apply(coastStep(step, rate, dt, tau)); moved = true; }
2388
2445
  return moved;
2389
2446
  },
2390
2447
 
2391
- /** Restore the pose captured at creation. */
2392
- home() { cameraCopy(cam, home); return orbit; },
2448
+ /** Restore the pose captured at creation; whatever was draining is dropped. */
2449
+ home() { cameraCopy(cam, home); cancel(); return orbit; },
2393
2450
 
2394
2451
  /** Remove the wheel listener, restore touch-action, unregister from the host. */
2395
2452
  dispose() {
2396
2453
  canvas.removeEventListener('wheel', onWheel);
2397
2454
  if (canvas.style) canvas.style.touchAction = touchAction;
2398
2455
  tracked.clear();
2456
+ cancel();
2399
2457
  host.unregister(orbit);
2400
2458
  return orbit;
2401
2459
  },