diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a7667186..ebbc56a8 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -31,7 +31,8 @@ module.exports = { "padded-blocks": ["off"], "indent": ["off"], "arrow-parens": ["off"], - "no-unused-vars": ["error"] + "no-unused-vars": ["error"], + "valid-jsdoc": ["off"], }, }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c4572b28..f329da60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@babel/eslint-parser": "7.22.11", "@babel/plugin-proposal-class-properties": "7.18.6", "@babel/preset-env": "7.22.10", + "@types/three": "^0.161.2", "babel-loader": "9.1.3", "eslint": "8.47.0", "eslint-config-google": "0.14.0", @@ -2025,6 +2026,30 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "dev": true }, + "node_modules/@types/stats.js": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", + "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==", + "dev": true + }, + "node_modules/@types/three": { + "version": "0.161.2", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.161.2.tgz", + "integrity": "sha512-DazpZ+cIfBzbW/p0zm6G8CS03HBMd748A3R1ZOXHpqaXZLv2I5zNgQUrRG//UfJ6zYFp2cUoCQaOLaz8ubH07w==", + "dev": true, + "dependencies": { + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.6.10", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.12.tgz", + "integrity": "sha512-+6LV7bN17XUWy4wIMILsGQX6ucawf64lYLG9jaGKSvOnKaJzWjcKXAkO0dZaC8MfoEqYQC7gl1GQnfITjBcazw==", + "dev": true + }, "node_modules/@typescript-eslint/parser": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", @@ -3323,6 +3348,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "dev": true + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4203,6 +4234,12 @@ "node": ">= 8" } }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "dev": true + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", diff --git a/package.json b/package.json index bad4dfac..d24307ed 100644 --- a/package.json +++ b/package.json @@ -8,22 +8,24 @@ "description": "Three.js-based 3D Gaussian splat viewer", "module": "build/gaussian-splats-3d.module.js", "main": "build/gaussian-splats-3d.umd.cjs", + "types": "build/index.d.ts", "author": "Mark Kellogg", "license": "MIT", "type": "module", "scripts": { + "build-types": "tsc", "build-demo": "mkdir -p ./build/demo && cp -r ./demo ./build/ && cp ./node_modules/three/build/three.module.js ./build/demo/lib/three.module.js", "build-library": "npx rollup -c && mkdir -p ./build/demo/lib && cp ./build/gaussian-splats-3d.module.* ./build/demo/lib/", - "build": "npm run build-library && npm run build-demo", + "build": "npm run build-library && npm run build-types && npm run build-demo", "build-demo-windows": "(if not exist \".\\build\\demo\" mkdir .\\build\\demo) && xcopy /E .\\demo .\\build\\demo && xcopy .\\node_modules\\three\\build\\three.module.js .\\build\\demo\\lib\\", "build-library-windows": "npx rollup -c && (if not exist \".\\build\\demo\\lib\" mkdir .\\build\\demo\\lib) && copy .\\build\\gaussian-splats-3d* .\\build\\demo\\lib\\", - "build-windows": "npm run build-library-windows && npm run build-demo-windows", + "build-windows": "npm run build-library-windows && npm run build-types && npm run build-demo-windows", "watch": "npx npm-watch ", "demo": "node util/server.js -d ./build/demo", "fix-styling": "npx stylelint **/*.scss --fix", "fix-js": "npx eslint src --fix", "lint": "npx eslint 'src/**/*.js' || true", - "prettify": "npx prettier --write 'src/**/*.js'" + "prettify": "npx prettier --write \"src/**/*.js\"" }, "watch": { "build-library": { @@ -52,6 +54,7 @@ "@babel/eslint-parser": "7.22.11", "@babel/plugin-proposal-class-properties": "7.18.6", "@babel/preset-env": "7.22.10", + "@types/three": "^0.161.2", "babel-loader": "9.1.3", "eslint": "8.47.0", "eslint-config-google": "0.14.0", diff --git a/src/DropInViewer.js b/src/DropInViewer.js index a0501566..a0c79f05 100644 --- a/src/DropInViewer.js +++ b/src/DropInViewer.js @@ -1,13 +1,26 @@ import * as THREE from 'three'; import { Viewer } from './Viewer.js'; +/** + * @typedef {Omit< + * import('./Viewer.js').ViewerOptions, + * | 'selfDrivenMode' + * | 'useBuiltInControls' + * | 'rootElement' + * | 'ignoreDevicePixelRatio' + * | 'dropInMode' + * | 'camera' + * | 'renderer' + * >} DropInViewerOptions + */ + /** * DropInViewer: Wrapper for a Viewer instance that enables it to be added to a Three.js scene like * any other Three.js scene object (Mesh, Object3D, etc.) */ export class DropInViewer extends THREE.Group { - constructor(options = {}) { + constructor(/** @type {DropInViewerOptions} */ options = {}) { super(); options.selfDrivenMode = false; @@ -18,8 +31,10 @@ export class DropInViewer extends THREE.Group { options.camera = undefined; options.renderer = undefined; + /** @type {import('./Viewer.js').Viewer} */ this.viewer = new Viewer(options); + /** @type {THREE.Mesh} */ this.callbackMesh = DropInViewer.createCallbackMesh(); this.add(this.callbackMesh); this.callbackMesh.onBeforeRender = DropInViewer.onBeforeRender.bind(this, this.viewer); @@ -29,22 +44,7 @@ export class DropInViewer extends THREE.Group { /** * Add a single splat scene to the viewer. * @param {string} path Path to splat scene to be loaded - * @param {object} options { - * - * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified - * value (valid range: 0 - 255), defaults to 1 - * - * showLoadingSpinner: Display a loading spinner while the scene is loading, defaults to true - * - * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] - * - * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] - * - * scale (Array): Scene's scale, defaults to [1, 1, 1] - * - * onProgress: Function to be called as file data are received - * - * } + * @param {import('./Viewer.js').AddSplatOptions} options * @return {AbortablePromise} */ addSplatScene(path, options = {}) { @@ -58,19 +58,7 @@ export class DropInViewer extends THREE.Group { /** * Add multiple splat scenes to the viewer. - * @param {Array} sceneOptions Array of per-scene options: { - * - * path: Path to splat scene to be loaded - * - * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified - * value (valid range: 0 - 255), defaults to 1 - * - * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] - * - * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] - * - * scale (Array): Scene's scale, defaults to [1, 1, 1] - * } + * @param {import('./Viewer.js').AddSplatsOptions} sceneOptions Array of per-scene options * @param {boolean} showLoadingSpinner Display a loading spinner while the scene is loading, defaults to true * @return {AbortablePromise} */ diff --git a/src/OrbitControls.js b/src/OrbitControls.js index 2d5e78f6..a7a0f236 100644 --- a/src/OrbitControls.js +++ b/src/OrbitControls.js @@ -124,6 +124,7 @@ class OrbitControls extends EventDispatcher { }; + /** @type {(domElement: HTMLElement) => void} */ this.listenToKeyEvents = function( domElement ) { domElement.addEventListener( 'keydown', onKeyDown ); @@ -131,6 +132,7 @@ class OrbitControls extends EventDispatcher { }; + /** @type {() => void} */ this.stopListenToKeyEvents = function() { this._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); diff --git a/src/SceneFormat.js b/src/SceneFormat.js index 239f7050..2ea75543 100644 --- a/src/SceneFormat.js +++ b/src/SceneFormat.js @@ -1,5 +1,5 @@ -export const SceneFormat = { +export const SceneFormat = /** @type {const} */ ({ 'Splat': 0, 'KSplat': 1, 'Ply': 2 -}; +}); diff --git a/src/SplatScene.js b/src/SplatScene.js index 9008cfe5..d3d00212 100644 --- a/src/SplatScene.js +++ b/src/SplatScene.js @@ -5,16 +5,26 @@ import * as THREE from 'three'; */ export class SplatScene { - constructor(splatBuffer, position = new THREE.Vector3(), quaternion = new THREE.Quaternion(), scale = new THREE.Vector3(1, 1, 1)) { + constructor( + /** @type {ArrayBuffer} */ splatBuffer, + position = new THREE.Vector3(), + quaternion = new THREE.Quaternion(), + scale = new THREE.Vector3(1, 1, 1) + ) { + /** @type {ArrayBuffer} */ this.splatBuffer = splatBuffer; + /** @type {THREE.Vector3} */ this.position = position.clone(); + /** @type {THREE.Quaternion} */ this.quaternion = quaternion.clone(); + /** @type {THREE.Vector3} */ this.scale = scale.clone(); + /** @type {THREE.Matrix4} */ this.transform = new THREE.Matrix4(); this.updateTransform(); } - copyTransformData(otherScene) { + copyTransformData(/** @type {THREE.Scene} */ otherScene) { this.position.copy(otherScene.position); this.quaternion.copy(otherScene.quaternion); this.scale.copy(otherScene.scale); diff --git a/src/Viewer.js b/src/Viewer.js index 5c902251..d62a5cd9 100644 --- a/src/Viewer.js +++ b/src/Viewer.js @@ -15,40 +15,86 @@ import { SceneFormat } from './SceneFormat.js'; const THREE_CAMERA_FOV = 50; const MINIMUM_DISTANCE_TO_NEW_FOCAL_POINT = .75; +/* eslint-disable max-len */ +/** + * @internal + * @typedef {object} _ViewerOptionsProps + * @prop {[x: number, y: number, z: number]} cameraUp The natural 'up' vector for viewing the scene (only has an effect when used with orbit controls and when the viewer uses its own camera). + * @prop {[x: number, y: number, z: number]} initialCameraPosition The camera's initial position (only used when the viewer uses its own camera). + * @prop {[x: number, y: number, z: number]} initialCameraLookAt The initial focal point of the camera and center of the camera's orbit (only used when the viewer uses its own camera). + */ + +/** + * @internal + * @typedef {object} _ViewerClassProps + * @prop {THREE.Vector3} cameraUp The natural 'up' vector for viewing the scene (only has an effect when used with orbit controls and when the viewer uses its own camera). + * @prop {THREE.Vector3} initialCameraPosition The camera's initial position (only used when the viewer uses its own camera). + * @prop {THREE.Vector3} initialCameraLookAt The initial focal point of the camera and center of the camera's orbit (only used when the viewer uses its own camera). + */ + +/** + * @internal + * @typedef {object} _ViewerCommonProps + * @prop {boolean} dropInMode 'dropInMode' is a flag that is used internally to support the usage of the viewer as a Three.js scene object + * @prop {boolean} selfDrivenMode If 'selfDrivenMode' is true, the viewer manages its own update/animation loop via requestAnimationFrame() + * @prop {THREE.WebGLRenderer} renderer Allows for usage of an external Three.js renderer + * @prop {THREE.Camera} camera Allows for usage of an external Three.js camera + * @prop {boolean} useBuiltInControls If 'useBuiltInControls' is true, the viewer will create its own instance of OrbitControls and attach to the camera + * @prop {boolean} ignoreDevicePixelRatio Tells the viewer to pretend the device pixel ratio is 1, which can boost performance on devices where it is larger, at a small cost to visual quality + * @prop {boolean} halfPrecisionCovariancesOnGPU Tells the viewer to use 16-bit floating point values when storing splat covariance data in textures, instead of 32-bit + * @prop {boolean} gpuAcceleratedSort If 'gpuAcceleratedSort' is true, a partially GPU-accelerated approach to sorting splats will be used. + * @prop {boolean} sharedMemoryForWorkers If 'sharedMemoryForWorkers' is true, a SharedArrayBuffer will be used to communicate with web workers. + * @prop {boolean} integerBasedSort If 'integerBasedSort' is true, the integer version of splat centers as well as other values used to calculate splat distances are used instead of the float version. + * @prop {boolean} dynamicScene If 'dynamicScene' is true, it tells the viewer to assume scene elements are not stationary or that the number of splats in the scene may change. + * @prop {HTMLElement} rootElement parent element of the Three.js renderer canvas + */ + +/** + * @typedef {Partial<_ViewerCommonProps & _ViewerOptionsProps>} ViewerOptions + */ + +/** + * @typedef AddSplatOptions + * @prop {number} [splatAlphaRemovalThreshold] Ignore any splats with an alpha less than the specified value (valid range: 0 - 255), defaults to `1` + * @prop {boolean} [showLoadingSpinner] Display a loading spinner while the scene is loading, defaults to `true` + * @prop {[x: number, y: number, z: number]} [position] Position of the scene, acts as an offset from its default position, defaults to `[0, 0, 0]` + * @prop {[x: number, y: number, z: number, w: number]} [rotation] Rotation of the scene represented as a quaternion, defaults to `[0, 0, 0, 1]` + * @prop {[x: number, y: number, z: number]} [scale] Scene's scale, defaults to `[1, 1, 1]` + * @prop {(totalPercent: number, percentLabel: string, label: string) => void} [onProgress] Function to be called as file data are received + */ + +/** @typedef {(Omit & { path: string })[]} AddSplatsOptions */ +/* eslint-enable max-len */ + /** * Viewer: Manages the rendering of splat scenes. Manages an instance of SplatMesh as well as a web worker * that performs the sort for its splats. + * + * @implements {ViewerCommonProps} + * @implements {ViewerClassProps} */ export class Viewer { - constructor(options = {}) { + constructor(/** @type {ViewerOptions} */ options = {}) { - // The natural 'up' vector for viewing the scene (only has an effect when used with orbit controls and - // when the viewer uses its own camera). if (!options.cameraUp) options.cameraUp = [0, 1, 0]; this.cameraUp = new THREE.Vector3().fromArray(options.cameraUp); - // The camera's initial position (only used when the viewer uses its own camera). if (!options.initialCameraPosition) options.initialCameraPosition = [0, 10, 15]; this.initialCameraPosition = new THREE.Vector3().fromArray(options.initialCameraPosition); - // The initial focal point of the camera and center of the camera's orbit (only used when the viewer uses its own camera). if (!options.initialCameraLookAt) options.initialCameraLookAt = [0, 0, 0]; this.initialCameraLookAt = new THREE.Vector3().fromArray(options.initialCameraLookAt); - // 'dropInMode' is a flag that is used internally to support the usage of the viewer as a Three.js scene object this.dropInMode = options.dropInMode || false; - // If 'selfDrivenMode' is true, the viewer manages its own update/animation loop via requestAnimationFrame() if (options.selfDrivenMode === undefined || options.selfDrivenMode === null) options.selfDrivenMode = true; this.selfDrivenMode = options.selfDrivenMode && !this.dropInMode; this.selfDrivenUpdateFunc = this.selfDrivenUpdate.bind(this); - // If 'useBuiltInControls' is true, the viewer will create its own instance of OrbitControls and attach to the camera if (options.useBuiltInControls === undefined) options.useBuiltInControls = true; this.useBuiltInControls = options.useBuiltInControls; - // parent element of the Three.js renderer canvas this.rootElement = options.rootElement; // Tells the viewer to pretend the device pixel ratio is 1, which can boost performance on devices where it is larger, @@ -242,6 +288,7 @@ export class Viewer { } } + /** @type {(e: KeyboardEvent) => void} */ onKeyDown = function() { const forward = new THREE.Vector3(); @@ -279,7 +326,7 @@ export class Viewer { }(); - onMouseMove(mouse) { + onMouseMove(/** @type {MouseEvent} */ mouse) { this.mousePosition.set(mouse.offsetX, mouse.offsetY); } @@ -288,6 +335,7 @@ export class Viewer { this.mouseDownTime = getCurrentTime(); } + /** @type {(this: this, mouse: MouseEvent) => void} */ onMouseUp = function() { const clickOffset = new THREE.Vector2(); @@ -308,6 +356,7 @@ export class Viewer { this.checkForFocalPointChange(); } + /** @type {() => void} */ checkForFocalPointChange = function() { const renderDimensions = new THREE.Vector2(); @@ -403,6 +452,7 @@ export class Viewer { this.renderer.domElement.parentElement.prepend(this.infoPanel); } + /** @type {() => void} */ updateSplatMesh = function() { const renderDimensions = new THREE.Vector2(); @@ -426,22 +476,7 @@ export class Viewer { /** * Add a splat scene to the viewer. * @param {string} path Path to splat scene to be loaded - * @param {object} options { - * - * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified - * value (valid range: 0 - 255), defaults to 1 - * - * showLoadingSpinner: Display a loading spinner while the scene is loading, defaults to true - * - * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] - * - * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] - * - * scale (Array): Scene's scale, defaults to [1, 1, 1] - * - * onProgress: Function to be called as file data are received - * - * } + * @param {AddSplatOptions} options * @return {AbortablePromise} */ addSplatScene(path, options = {}) { @@ -483,21 +518,9 @@ export class Viewer { /** * Add multiple splat scenes to the viewer. - * @param {Array} sceneOptions Array of per-scene options: { - * - * path: Path to splat scene to be loaded - * - * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified - * value (valid range: 0 - 255), defaults to 1 - * - * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] - * - * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] - * - * scale (Array): Scene's scale, defaults to [1, 1, 1] - * } + * @param {AddSplatsOptions} sceneOptions Array of per-scene options * @param {boolean} showLoadingSpinner Display a loading spinner while the scene is loading, defaults to true - * @param {function} onProgress Function to be called as file data are received + * @param {AddSplatOptions['onProgress']} onProgress Function to be called as file data are received * @return {AbortablePromise} */ addSplatScenes(sceneOptions, showLoadingSpinner = true, onProgress = undefined) { @@ -556,8 +579,9 @@ export class Viewer { * @param {number} splatAlphaRemovalThreshold Ignore any splats with an alpha less than the specified * value (valid range: 0 - 255), defaults to 1 * - * @param {function} onProgress Function to be called as file data are received - * @param {string} format Optional format specifier, if not specified the format will be inferred from the file extension + * @param {AddSplatOptions['onProgress']} onProgress Function to be called as file data are received + * @param {(typeof SceneFormat)[keyof typeof SceneFormat]} format + * Optional format specifier, if not specified the format will be inferred from the file extension * @return {AbortablePromise} */ loadFileToSplatBuffer(path, splatAlphaRemovalThreshold = 1, onProgress = undefined, format = undefined) { @@ -584,6 +608,13 @@ export class Viewer { /** * Add one or more instances of SplatBuffer to the SplatMesh instance managed by the viewer and set up the sorting web worker. * This function will terminate the existing sort worker (if there is one). + * + * @type {( + * this: this, + * splatBuffers: ArrayBuffer[], + * splatBufferOptions?: any[], + * showLoadingSpinner?: boolean + * ) => Promise} */ addSplatBuffers = function() { @@ -829,6 +860,7 @@ export class Viewer { this.init(); } + /** @type {() => void} */ updateFPS = function() { let lastCalcTime = getCurrentTime(); @@ -883,6 +915,7 @@ export class Viewer { }(); + /** @type {(currentTime: number) => void} */ updateCameraTransition = function() { let tempCameraTarget = new THREE.Vector3(); @@ -1013,6 +1046,7 @@ export class Viewer { } } + /** @type {(force?: boolean, gatherAllNodes?: boolean) => Promise} */ updateSplatSort = function() { const mvpMatrix = new THREE.Matrix4(); diff --git a/src/raycaster/Raycaster.js b/src/raycaster/Raycaster.js index f78771d0..48dfcbbf 100644 --- a/src/raycaster/Raycaster.js +++ b/src/raycaster/Raycaster.js @@ -9,6 +9,13 @@ export class Raycaster { this.raycastAgainstTrueSplatEllipsoid = raycastAgainstTrueSplatEllipsoid; } + /** + * @type {( + * camera: THREE.Camera, + * screenPosition: THREE.Vector2, + * screenDimensions: THREE.Vector2 + * ) => void} + * */ setFromCameraAndScreenPosition = function() { const ndcCoords = new THREE.Vector2(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..0f124e8e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "include": ["src/index.js"], + "compilerOptions": { + "allowJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "build", + "declarationMap": true, + } +} \ No newline at end of file