diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..c11c416 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,26 @@ +name: Run code quality tooling + +on: + pull_request: + branches: [main] + workflow_call: + +jobs: + static-analysis: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Typecheck + run: npm run typecheck + - name: Lint + run: npm run lint + - name: Check formatting + run: npm run format:check diff --git a/.github/workflows/static.yml b/.github/workflows/deploy.yml similarity index 81% rename from .github/workflows/static.yml rename to .github/workflows/deploy.yml index 9432148..5f062f9 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/deploy.yml @@ -21,8 +21,12 @@ concurrency: cancel-in-progress: true jobs: + check-code-quality: + uses: ./.github/workflows/code-quality.yml + # Single deploy job since we're just deploying deploy: + needs: [check-code-quality] environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} @@ -33,13 +37,14 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: 'npm' - name: Install dependencies run: npm ci - name: Build env: - GITHUB_ACTIONS_BASE: ${{ github.event.repository.name }} + # Base URL to be passed into Vite + GITHUB_ACTIONS_BASE: /${{ github.event.repository.name }} run: npm run build - name: Setup Pages uses: actions/configure-pages@v4 @@ -50,4 +55,4 @@ jobs: path: './dist' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..83f5b58 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "printWidth": 100, + "singleQuote": true, + "endOfLine": "auto", + "semi": true, + "tabWidth": 2, + "trailingComma": "all" +} diff --git a/INSTRUCTION.md b/INSTRUCTION.md index 9c54787..6cf7e60 100644 --- a/INSTRUCTION.md +++ b/INSTRUCTION.md @@ -1,5 +1,4 @@ -WebGPU WebGPU Gaussian Splat Viewer Instructions -========================================================== +# WebGPU WebGPU Gaussian Splat Viewer Instructions **This is due Tuesday, October 28th at 11:59 PM.** @@ -9,15 +8,16 @@ In this project, you will implement the 3D Gaussian Splat Viewer. You are given - `src/` contains all the TypeScript and WGSL code for this project. This contains several subdirectories: - `renderers/` defines the different renderers in which you will implement gaussian renderer. - - `shaders/` contains the WGSL files that are interpreted as shader programs at runtime. + - `shaders/` contains the WGSL files that are interpreted as shader programs at runtime. - `camera/` includes camera controls, camera file loading. - - `utils/` includes PLY file loading, utility functions for debugging. + - `utils/` includes PLY file loading, utility functions for debugging. Scene Files: download scene files from [drive](https://drive.google.com/drive/folders/1Fz0QhyDU12JTsl2e7umGi5iy_V9drrIW?usp=sharing). ## Running the code Follow these steps to install and view the project: + - Clone this repository - Download and install [Node.js](https://nodejs.org/en/) - Run `npm install` in the root directory of this project to download and install dependencies @@ -45,6 +45,7 @@ Follow these steps to install and view the project: ### GitHub Pages setup Since this project uses WebGPU, it is easy to deploy it on the web for anyone to see. To set this up, do the following: + - Go to your repository's settings - Go to the "Pages" tab - Under "Build and Deployment", set "Source" to "GitHub Actions" @@ -58,6 +59,7 @@ Once you've done those steps, any new commit to the `main` branch should automat **Ask on Ed Discussion for any clarifications.** In this project, you are given code for: + - ply scene loading - camera json file loading - radix sort compute shader @@ -69,39 +71,42 @@ For editing the project, you will want to use [Visual Studio Code](https://code. WebGPU errors will appear in your browser's developer console (Ctrl + Shift + J for Chrome on Windows). Unlike some other graphics APIs, WebGPU error messages are often very helpful, especially if you've labeled your various pipeline components with meaningful names. Be sure to check the console whenever something isn't working correctly. ### Part 1: Understanding 3D Gaussian Point Cloud & Add MVP calculation (10pts) -- Read over the [3D Gaussian Splatting Paper](https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/) to have a basic understanding. -- Then read over `point_cloud` renderer, add MVP calculation to the vertex shader. After that, you can see yellow point cloud rendered to screen. + +- Read over the [3D Gaussian Splatting Paper](https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/) to have a basic understanding. +- Then read over `point_cloud` renderer, add MVP calculation to the vertex shader. After that, you can see yellow point cloud rendered to screen. ### Part 2: Gaussian Renderer (80pts total)(50pts(preprocess)+30pts(renderer)) -#### Gaussian Renderer Pipeline Overview: - - Loading 3D gaussian data into GPU (this part is done for you, see `PointCloud` in load.ts) - - Preprocess 3D gaussian data - - Implement view frustum culling to remove non-visible Gaussians (make bounding box to be slightly larger to keep the edge gaussians) - - Compute 3D covariance based on rotation and scale, also user inputted gaussian multipler. (see [post](https://github.com/kwea123/gaussian_splatting_notes) on 1.1 section) - - Compute 2D conic, maximum radius, and maximum quad size in NDC (see [post](https://github.com/kwea123/gaussian_splatting_notes) on 1.1 section) - - Using spherical harmonics coeffiecients to evaluate the color of the gaussian from particular view direction (evaluation function is provided, see [post](https://beatthezombie.github.io/sh_post_1/) ). - - Store essential 2D gaussian data for later rasteriation pipeline - - Add key_size, indices, and depth to sorter. - - Sort Gaussians based on depth - - Render the 2D splat on quad utlizing indirect draw call (instance count from process step) in sorted order. - - vertex shader: reconstruct 2D quad vertices (NDC) from splat data, send conic and color information to fragment shader - - fragment shader: using conic matrix [see "Centered matrix equation"](https://en.wikipedia.org/wiki/Matrix_representation_of_conic_sections) and [CUDA implementation](https://github.com/graphdeco-inria/diff-gaussian-rasterization/blob/main/cuda_rasterizer/forward.cu#L330) to determine whether point is inside splat. The opacity should decade exponentially as it distant from center. +#### Gaussian Renderer Pipeline Overview: + +- Loading 3D gaussian data into GPU (this part is done for you, see `PointCloud` in load.ts) +- Preprocess 3D gaussian data + - Implement view frustum culling to remove non-visible Gaussians (make bounding box to be slightly larger to keep the edge gaussians) + - Compute 3D covariance based on rotation and scale, also user inputted gaussian multipler. (see [post](https://github.com/kwea123/gaussian_splatting_notes) on 1.1 section) + - Compute 2D conic, maximum radius, and maximum quad size in NDC (see [post](https://github.com/kwea123/gaussian_splatting_notes) on 1.1 section) + - Using spherical harmonics coeffiecients to evaluate the color of the gaussian from particular view direction (evaluation function is provided, see [post](https://beatthezombie.github.io/sh_post_1/) ). + - Store essential 2D gaussian data for later rasteriation pipeline + - Add key_size, indices, and depth to sorter. +- Sort Gaussians based on depth +- Render the 2D splat on quad utlizing indirect draw call (instance count from process step) in sorted order. + - vertex shader: reconstruct 2D quad vertices (NDC) from splat data, send conic and color information to fragment shader + - fragment shader: using conic matrix [see "Centered matrix equation"](https://en.wikipedia.org/wiki/Matrix_representation_of_conic_sections) and [CUDA implementation](https://github.com/graphdeco-inria/diff-gaussian-rasterization/blob/main/cuda_rasterizer/forward.cu#L330) to determine whether point is inside splat. The opacity should decade exponentially as it distant from center. #### Implementation Hints: + - scene file information: - `opacity`: use [sigmoid](https://github.com/graphdeco-inria/diff-gaussian-rasterization/blob/59f5f77e3ddbac3ed9db93ec2cfe99ed6c5d121d/cuda_rasterizer/auxiliary.h#L134) function to bring back maximum opacity - `scale` (log space): use exponential function to bring back actual scale - useful shader functions: - - `unpack2x16float`: all gaussian data is packed in f16, need to unpacked it in shader. + - `unpack2x16float`: all gaussian data is packed in f16, need to unpacked it in shader. - `pack2x16float`: pack you 2D gaussian data in f16 format - `atomicAdd` : store indices in sorting buffer using thread-safe updates in compute shaders - `arrayLength`: remember to check thread idx is within the numbers of gaussians - Setting up pipeline: - - `device.queue.writeBuffer`: Remember to clean sort infos each frame. + - `device.queue.writeBuffer`: Remember to clean sort infos each frame. - `encoder.copyBufferToBuffer`: GPU buffer transfer data to other GPU buffer - - `blend`: using similar blending function as rendering semi-transparent texture to screen. - - `depth`: similarly to semi-transparent texture, we should render gaussians back to front. + - `blend`: using similar blending function as rendering semi-transparent texture to screen. + - `depth`: similarly to semi-transparent texture, we should render gaussians back to front. #### Implementation Step Recommendation: @@ -112,11 +117,11 @@ WebGPU errors will appear in your browser's developer console (Ctrl + Shift + J Before working on the preprocessing compute pipeline, you should first set up the indirect rendering pipeline that replicates the point cloud rendering you did in part 1. The recommended workflow is as follows: 1. Set up the indirect draw buffer and the render pipeline, and render pc.num_points number of quads (with smaller size such as 0.01 x 0.01) on the screen. -2. Finish the compute pipeline by binding the correct buffers. In the preprocess shader, transform the Gaussian points from world space to NDC space and stored data in Splat (similar to point cloud renderer). In the render pipeline, read from this Splat buffer to determine the position for drawing the quad. +2. Finish the compute pipeline by binding the correct buffers. In the preprocess shader, transform the Gaussian points from world space to NDC space and stored data in Splat (similar to point cloud renderer). In the render pipeline, read from this Splat buffer to determine the position for drawing the quad. 3. Now, implement a simple view-frustum culling (bounding box of 1.2x screen size is recommended). Instead of drawing pc.num_points quads through the hard-coded indirect buffer, use `atomicAdd` on `sort_infos.keys_size` to determine how many quads to draw. After the preprocess shader, use `sort_infos.keys_size` to update the indirect draw buffer's instance count. Now the total number of quads to render is determined by the preprocess shader. 4. The final step for setup is to bind the render settings as a buffer, as declared in the shader. The Gaussian scaling member variable should be adjustable on the host side using a scroll bar (see in `renderer.ts`). -To test whether the buffer is working, use the Gaussian scaling member to influence the quad size, allowing you to adjust the size of the quads with the scroll bar. Also, be careful that complier might optimize away shader binding buffer if you do not actually use it in shader functions. +To test whether the buffer is working, use the Gaussian scaling member to influence the quad size, allowing you to adjust the size of the quads with the scroll bar. Also, be careful that complier might optimize away shader binding buffer if you do not actually use it in shader functions. Preprocess Compute Setup Suggestions: @@ -124,7 +129,7 @@ Now, implement the computation of the 3D covariance, 2D covariance, and finally ![quad_white](./images/quad_white.png) -After computing the radius, you can implement sorting by specifying `sort_depths` and `sort_indices` for each visible Gaussian. Also, remember to use `atomicAdd` on `sort_dispatch.dispatch_x` every time the visible Gaussian count exceeds the total number of sorting threads you will dispatch. To visualize the sorting result, assign each quad a color determined by its size, for example, `(width, height, 0.0, 1.0)` as the RGBA. +After computing the radius, you can implement sorting by specifying `sort_depths` and `sort_indices` for each visible Gaussian. Also, remember to use `atomicAdd` on `sort_dispatch.dispatch_x` every time the visible Gaussian count exceeds the total number of sorting threads you will dispatch. To visualize the sorting result, assign each quad a color determined by its size, for example, `(width, height, 0.0, 1.0)` as the RGBA. ![quad_green](./images/quad_green.png) @@ -140,72 +145,84 @@ To determine whether a point is inside a splat, you need to compute the offset f ![quad_pixeldiff](./images/quad_pixeldiff.png) -### Part 3: Extra Credit: +### Part 3: Extra Credit: #### Optimization: tile-based depth sorting (20pts) -Follow the [paper](https://github.com/kwea123/gaussian_splatting_notes) implementation using tile-based depth sorting. Then composite the final image using compute shader. +Follow the [paper](https://github.com/kwea123/gaussian_splatting_notes) implementation using tile-based depth sorting. Then composite the final image using compute shader. ![Gaussian with Tile](./images/sorting2.webp) ![Gaussian with Tile](./images/sorting1.webp) #### Optimization: half-precision floating point calculation (10pts) -See the [WebGPU supported f16 function](https://webgpufundamentals.org/webgpu/lessons/webgpu-wgsl-function-reference.html), implement your compressed f16 compute shader for preprocess step. +See the [WebGPU supported f16 function](https://webgpufundamentals.org/webgpu/lessons/webgpu-wgsl-function-reference.html), implement your compressed f16 compute shader for preprocess step. ## Performance Analysis (10pts) #### Answer these questions: + - Compare your results from point-cloud and gaussian renderer, what are the differences? - For gaussian renderer, how does changing the workgroup-size affect performance? Why do you think this is? -- Does view-frustum culling give performance improvement? Why do you think this is? -- Does number of guassians affect performance? Why do you think this is? +- Does view-frustum culling give performance improvement? Why do you think this is? +- Does number of guassians affect performance? Why do you think this is? ## Base Code Walkthrough In general, you can search for comments containing "TODO" to see the most important/useful parts of the base code. #### `renderers/` + Defines the different renderers for Gaussian splats and point clouds. + - **`gaussian-renderer.ts`**: Implements the renderer for Gaussian splats. - **`point-cloud-renderer.ts`**: Implements the renderer for point clouds. - **`renderer.ts`**: Abstract base render app for common renderer functionality, as well as GUI. #### `shaders/` + Contains WGSL shader programs that run on the GPU for rendering. + - **`gaussian.wgsl`**: Vertex and fragment shaders for Gaussian splat rendering. - **`point_cloud.wgsl`**: Shaders for rendering point cloud data. - **`preprocess.wgsl`**: Preprocesses Gaussian data for rendering. #### `camera/` + Manages camera controls and view/projection transformations. + - **`camera.ts`**: Handles the camera's view and projection matrices. - **`camera-control.ts`**: Implements user input handling for camera movement. #### `utils/` + Includes PLY file loading and utility functions for debugging and GPU management. + - **`load.ts`**: Loads Gaussian PLY data, and sets up WebGPU buffers. - **`plyreader.ts`**: Parses PLY files to extract point cloud data. - **`simple-console.ts`**: Provides basic logging and debugging utilities. - **`util.ts`**: Provides utility functions for assertions, error handling, and alignment operations. #### `sort/` + Handles sorting operations for rendering transparency in Gaussian splats. + - **`sort.ts`**: Interfaces with the radix sort WGSL shader to sort Gaussian splats by depth. - **`radix_sort.wgsl`**: Implements parallel radix sort on the GPU. #### `main.ts` + The entry point of the project, responsible for initializing WebGPU. ## README Replace the contents of `README.md` with the following: + - A brief description of your project and the specific features you implemented - At least one screenshot of your project running - A 30+ second video/gif of your project running showing all features (even though your demo can be seen online, it may not run on all computers, while a video will work everywhere) - A link to your project's live website (see [GitHub Pages setup](#github-pages-setup)) - ## Submit Open a GitHub pull request so that we can see that you have finished. The title should be "Project 4: YOUR NAME". The template of the comment section of your pull request is attached below, you can do some copy and paste: diff --git a/README.md b/README.md index edffdaf..0cdcb38 100644 --- a/README.md +++ b/README.md @@ -2,25 +2,29 @@ **University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 5** -* (TODO) YOUR NAME HERE -* Tested on: (TODO) **Google Chrome 222.2** on - Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab) +- Thomas Shaw +- Tested on: **Google Chrome 139.0** on + Windows 11, Ryzen 7 5700x @ 4.67GHz, 32GB, RTX 2070 8GB -### Live Demo - -[![](img/thumb.png)](http://TODO.github.io/Project4-WebGPU-Forward-Plus-and-Clustered-Deferred) +### [Live Demo](https://printer83mph.github.io/CIS5650-Project5-WebGPU-Gaussian-Splat-Viewer/) ### Demo Video/GIF -[![](img/video.mp4)](TODO) + + +### Analysis + +- Comparing our point cloud renderer to our gaussian splat renderer, we find a fairly large degredation in performance. This makes sense — our point cloud renderer only has to draw points, while the gaussian pipeline must run depth sorting, covariance computation, and overdraw many pixels from each overlapping splat quad. -### (TODO: Your README) +- Updating the workgroup size leads to strange behavior. This may be due to my implementation, but it seems that any workgroup size outside of 256 will lead to depth sorting artifacts and reduced performance. It may be that smaller workgroups greatly increase the required depth of the parallel radix sort. + - 64: 30-40fps, rendering artifacts + - 128: 50-100fps, rendering artifacts + - 256: 80-120fps + - \> 256: invalid size for webgpu -*DO NOT* leave the README to the last minute! It is a crucial part of the -project, and we will not be able to grade you without a good README. +- View-frustum culling gives a small performance boost only when looking away from some of the scene. It seems that a large part of the performance cost is simply drawing all of the quads. -This assignment has a considerable amount of performance analysis compared -to implementation work. Complete the implementation early to leave time! +- With a larger number of gaussians, scene load time increases dramatically, and performance degrades quite a bit. However, of course, it is only when facing the model and thus drawing these quads that the performance hit is seen. ### Credits diff --git a/docs/img/point_cloud.png b/docs/img/point_cloud.png new file mode 100644 index 0000000..37344fd Binary files /dev/null and b/docs/img/point_cloud.png differ diff --git a/docs/img/uhhhh_01.png b/docs/img/uhhhh_01.png new file mode 100644 index 0000000..fa58ae3 Binary files /dev/null and b/docs/img/uhhhh_01.png differ diff --git a/docs/video/bonsai.webp b/docs/video/bonsai.webp new file mode 100644 index 0000000..2c096e7 Binary files /dev/null and b/docs/video/bonsai.webp differ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..47ea983 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,20 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; +import { defineConfig } from 'eslint/config'; +import eslintConfigPrettier from 'eslint-config-prettier/flat'; + +export default defineConfig([ + { + files: ['**/*.{js,mjs,cjs,ts,mts,cts}'], + plugins: { js }, + extends: ['js/recommended'], + languageOptions: { globals: { ...globals.browser } }, + }, + { + files: ['*.config.{js,ts}'], + languageOptions: { globals: { ...globals.node } }, + }, + tseslint.configs.recommended, + eslintConfigPrettier, +]); diff --git a/index.html b/index.html index 4067a69..1ec46ca 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + diff --git a/package-lock.json b/package-lock.json index 04843bd..3311c69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,13 +12,21 @@ "@loaders.gl/ply": "^4.2.2", "@petamoriken/float16": "^3.8.7", "tweakpane": "^3.1.8", + "tweakpane-plugin-file-import": "^0.2.0", "wgpu-matrix": "^3.2.0" }, "devDependencies": { + "@eslint/js": "^9.38.0", "@tweakpane/core": "^1.1.7", + "@types/node": "^24.9.1", "@webgpu/types": "^0.1.31", + "eslint": "^9.38.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^16.4.0", + "prettier": "^3.6.2", "tweakpane-plugin-file-import": "^0.2.0", "typescript": "^5.0.4", + "typescript-eslint": "^8.46.2", "vite": "^4.3.1", "vite-raw-plugin": "^1.0.1" } @@ -375,6 +383,215 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@loaders.gl/core": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.2.2.tgz", @@ -430,6 +647,44 @@ "@loaders.gl/core": "^4.0.0" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@petamoriken/float16": { "version": "3.8.7", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.8.7.tgz", @@ -459,165 +714,1494 @@ "integrity": "sha512-9tq+KAhaqPiOgsFyLPAz1IMXkVfhRqxGzAgy1ps3As6o3W7XjnU7sev6OlD/Z+Pzw8uZVMukkSHf2e0uCU6u0A==", "dev": true }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/geojson": { "version": "7946.0.14", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" }, - "node_modules/@webgpu/types": { - "version": "0.1.44", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz", - "integrity": "sha512-JDpYJN5E/asw84LTYhKyvPpxGnD+bAKPtpW9Ilurf7cZpxaTbxkQcGwOd7jgB9BPBrTYQ+32ufo4HiuomTjHNQ==", - "dev": true + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "node_modules/@types/node": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 4" } }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "node_modules/@typescript-eslint/parser": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true - }, - "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", "dev": true, - "bin": { - "rollup": "dist/bin/rollup" + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" }, "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tweakpane": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-3.1.10.tgz", - "integrity": "sha512-rqwnl/pUa7+inhI2E9ayGTqqP0EPOOn/wVvSWjZsRbZUItzNShny7pzwL3hVlaN4m9t/aZhsP0aFQ9U5VVR2VQ==", + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "url": "https://github.com/sponsors/cocopon" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/tweakpane-plugin-file-import": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tweakpane-plugin-file-import/-/tweakpane-plugin-file-import-0.2.1.tgz", - "integrity": "sha512-8v9EFMKkyVXf5pu5xMdR8TAEBZ4qizMyd+1NN5fbdPN51KD28PFhDCbZcIvlDPPz05Y75MVhv0nJ7Cbk4Cbb5Q==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, "peerDependencies": { - "tweakpane": "^3.1.4" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "node_modules/@typescript-eslint/types": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.44", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz", + "integrity": "sha512-JDpYJN5E/asw84LTYhKyvPpxGnD+bAKPtpW9Ilurf7cZpxaTbxkQcGwOd7jgB9BPBrTYQ+32ufo4HiuomTjHNQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tweakpane": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-3.1.10.tgz", + "integrity": "sha512-rqwnl/pUa7+inhI2E9ayGTqqP0EPOOn/wVvSWjZsRbZUItzNShny7pzwL3hVlaN4m9t/aZhsP0aFQ9U5VVR2VQ==", + "funding": { + "url": "https://github.com/sponsors/cocopon" + } + }, + "node_modules/tweakpane-plugin-file-import": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tweakpane-plugin-file-import/-/tweakpane-plugin-file-import-0.2.1.tgz", + "integrity": "sha512-8v9EFMKkyVXf5pu5xMdR8TAEBZ4qizMyd+1NN5fbdPN51KD28PFhDCbZcIvlDPPz05Y75MVhv0nJ7Cbk4Cbb5Q==", + "dev": true, + "peerDependencies": { + "tweakpane": "^3.1.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "bin": { @@ -628,6 +2212,47 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz", + "integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.2", + "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vite": { "version": "4.5.5", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", @@ -693,6 +2318,45 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/wgpu-matrix/-/wgpu-matrix-3.2.0.tgz", "integrity": "sha512-0Ac1QiZFICoOyLHCWETXSPkWx9mVMAs1z98smywAm67vyzNkmaiHVqzrlm2emrzMZbmx6tO8DCOjtwer0ZnDiQ==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 8a751f0..9c0f109 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,25 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "typecheck": "tsc", + "lint": "eslint . --max-warnings=0", + "lint:fix": "eslint . --fix", + "format": "prettier -w .", + "format:check": "prettier . --check" }, "devDependencies": { + "@eslint/js": "^9.38.0", "@tweakpane/core": "^1.1.7", - "tweakpane-plugin-file-import": "^0.2.0", + "@types/node": "^24.9.1", "@webgpu/types": "^0.1.31", + "eslint": "^9.38.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^16.4.0", + "prettier": "^3.6.2", + "tweakpane-plugin-file-import": "^0.2.0", "typescript": "^5.0.4", + "typescript-eslint": "^8.46.2", "vite": "^4.3.1", "vite-raw-plugin": "^1.0.1" }, diff --git a/src/camera/camera-control.ts b/src/camera/camera-control.ts index f6d6d99..eebaf76 100644 --- a/src/camera/camera-control.ts +++ b/src/camera/camera-control.ts @@ -1,4 +1,4 @@ -import { vec3, mat3, mat4, quat } from 'wgpu-matrix'; +import { vec3, mat4, quat } from 'wgpu-matrix'; import { Camera } from './camera'; export class CameraControl { @@ -20,7 +20,9 @@ export class CameraControl { this.element.addEventListener('pointermove', this.moveCallback.bind(this)); this.element.addEventListener('pointerup', this.upCallback.bind(this)); this.element.addEventListener('wheel', this.wheelCallback.bind(this)); - this.element.addEventListener('contextmenu', (e) => { e.preventDefault(); }); + this.element.addEventListener('contextmenu', (e) => { + e.preventDefault(); + }); } private panning = false; @@ -91,4 +93,4 @@ export class CameraControl { vec3.add(d, this.camera.position, this.camera.position); this.camera.update_buffer(); } -}; \ No newline at end of file +} diff --git a/src/camera/camera.ts b/src/camera/camera.ts index 47ea1dc..196dfc0 100644 --- a/src/camera/camera.ts +++ b/src/camera/camera.ts @@ -1,21 +1,22 @@ -import { Mat3, mat3, Mat4, mat4, Vec3, vec3, Vec2, vec2 } from 'wgpu-matrix'; -import { log, time, timeLog } from '../utils/simple-console'; +import { mat3, Mat4, mat4, Vec3, vec3, Vec2, vec2 } from 'wgpu-matrix'; +import { log } from '../utils/simple-console'; interface CameraJson { - id: number - img_name: string - width: number - height: number - position: number[] - rotation: number[][] - fx: number - fy: number -}; + id: number; + img_name: string; + width: number; + height: number; + position: number[]; + rotation: number[][]; + fx: number; + fy: number; +} function focal2fov(focal: number, pixels: number): number { return 2 * Math.atan(pixels / (2 * focal)); } +// eslint-disable-next-line @typescript-eslint/no-unused-vars function fov2focal(fov: number, pixels: number): number { return pixels / (2 * Math.tan(fov * 0.5)); } @@ -25,12 +26,11 @@ function get_view_matrix(r: Mat4, t: Vec3): Mat4 { return mat4.translate(r, minus_t); } - function get_projection_matrix(znear: number, zfar: number, fov_x: number, fov_y: number) { // return mat4.perspective(fov_y, 1, znear, zfar); - const tan_half_fov_y = Math.tan(fov_y / 2.); - const tan_half_fov_x = Math.tan(fov_x / 2.); + const tan_half_fov_y = Math.tan(fov_y / 2); + const tan_half_fov_x = Math.tan(fov_x / 2); const top = tan_half_fov_y * znear; const bottom = -top; @@ -38,16 +38,16 @@ function get_projection_matrix(znear: number, zfar: number, fov_x: number, fov_y const left = -right; const p = mat4.create(); - p[0] = 2.0 * znear / (right - left); + p[0] = (2.0 * znear) / (right - left); // p[5] = 2.0 * znear / (top - bottom); - p[5] = -2.0 * znear / (top - bottom); // flip Y + p[5] = (-2.0 * znear) / (top - bottom); // flip Y p[2] = (right + left) / (right - left); p[6] = (top + bottom) / (top - bottom); - p[14] = 1.; + p[14] = 1; p[10] = zfar / (zfar - znear); p[11] = -(zfar * znear) / (zfar - znear); mat4.transpose(p, p); - + // p[0] = 2.0 * znear / (right - left); // p[5] = 2.0 * znear / (top - bottom); // p[8] = (right + left) / (right - left); @@ -60,20 +60,20 @@ function get_projection_matrix(znear: number, zfar: number, fov_x: number, fov_y } interface CameraPreset { - position: Vec3, - rotation: Mat4, + position: Vec3; + rotation: Mat4; } export async function load_camera_presets(file: string): Promise { const blob = new Blob([file]); const arrayBuffer = await new Promise((resolve, reject) => { const reader = new FileReader(); - - reader.onload = function(event) { - resolve(event.target.result); // Resolve the promise with the ArrayBuffer + + reader.onload = function (event) { + resolve(event.target.result); // Resolve the promise with the ArrayBuffer }; - reader.onerror = reject; // Reject the promise in case of an error + reader.onerror = reject; // Reject the promise in case of an error reader.readAsArrayBuffer(blob); }); const text = new TextDecoder().decode(arrayBuffer as ArrayBuffer); @@ -92,18 +92,18 @@ export async function load_camera_presets(file: string): Promise }); } - const c_size_vec2 = 4 * 2; const c_size_mat4 = 4 * 16; // byte size of mat4 (i.e. Float32Array(16)) const c_size_camera_uniform = 4 * c_size_mat4 + 2 * c_size_vec2; +// eslint-disable-next-line @typescript-eslint/no-unused-vars interface CameraUniform { - view_matrix: Mat4, - view_inv_matrix: Mat4, - proj_matrix: Mat4, - proj_inv_matrix: Mat4, + view_matrix: Mat4; + view_inv_matrix: Mat4; + proj_matrix: Mat4; + proj_inv_matrix: Mat4; - viewport: Vec2, - focal: Vec2, + viewport: Vec2; + focal: Vec2; } export function create_camera_uniform_buffer(device: GPUDevice) { @@ -114,7 +114,9 @@ export function create_camera_uniform_buffer(device: GPUDevice) { }); } -const intermediate_float_32_array = new Float32Array(c_size_camera_uniform / Float32Array.BYTES_PER_ELEMENT); +const intermediate_float_32_array = new Float32Array( + c_size_camera_uniform / Float32Array.BYTES_PER_ELEMENT, +); export class Camera { constructor( @@ -126,7 +128,7 @@ export class Camera { } on_update_canvas(): void { - const focal = 0.5 * this.canvas.height / Math.tan(this.fovY * 0.5); + const focal = (0.5 * this.canvas.height) / Math.tan(this.fovY * 0.5); this.focal[0] = focal; this.focal[1] = focal; this.fovX = focal2fov(focal, this.canvas.width); @@ -136,12 +138,12 @@ export class Camera { this.update_buffer(); } - + readonly uniform_buffer: GPUBuffer; - + position = vec3.create(); rotation = mat4.create(); - private fovY: number = 45 / 180 * Math.PI; + private fovY: number = (45 / 180) * Math.PI; private fovX: number; private focal: Vec2 = vec2.create(); private viewport: Vec2 = vec2.create(); @@ -186,5 +188,4 @@ export class Camera { mat4.copy(preset.rotation, this.rotation); this.update_buffer(); } - -}; +} diff --git a/src/main.ts b/src/main.ts index 25efcb5..69e7383 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,8 @@ -import './style.css' +import './style.css'; import init from './renderers/renderer'; import { assert } from './utils/util'; (async () => { - if (navigator.gpu === undefined) { const h = document.querySelector('#title') as HTMLElement; h.innerText = 'WebGPU is not supported in this browser.'; @@ -17,18 +16,17 @@ import { assert } from './utils/util'; h.innerText = 'No adapter is available for WebGPU.'; return; } - + const device = await adapter.requestDevice({ - requiredLimits: { + requiredLimits: { maxComputeWorkgroupStorageSize: adapter.limits.maxComputeWorkgroupStorageSize, - maxStorageBufferBindingSize: adapter.limits. maxStorageBufferBindingSize - }, + maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize, + }, }); const canvas = document.querySelector('#webgpu-canvas'); assert(canvas !== null); const context = canvas.getContext('webgpu') as GPUCanvasContext; - + init(canvas, context, device); - -})(); \ No newline at end of file +})(); diff --git a/src/renderers/gaussian-renderer.ts b/src/renderers/gaussian-renderer.ts index 1684523..bbc1d27 100644 --- a/src/renderers/gaussian-renderer.ts +++ b/src/renderers/gaussian-renderer.ts @@ -1,47 +1,140 @@ import { PointCloud } from '../utils/load'; -import preprocessWGSL from '../shaders/preprocess.wgsl'; -import renderWGSL from '../shaders/gaussian.wgsl'; -import { get_sorter,c_histogram_block_rows,C } from '../sort/sort'; +import preprocessWGSL from '../shaders/preprocess.wgsl?raw'; +import renderWGSL from '../shaders/gaussian.wgsl?raw'; +import { get_sorter, c_histogram_block_rows, C } from '../sort/sort'; import { Renderer } from './renderer'; export interface GaussianRenderer extends Renderer { - + updateRenderSettings(settings: { gaussianScaling?: number; shDeg?: number }): void; } -// Utility to create GPU buffers -const createBuffer = ( - device: GPUDevice, - label: string, - size: number, - usage: GPUBufferUsageFlags, - data?: ArrayBuffer | ArrayBufferView -) => { - const buffer = device.createBuffer({ label, size, usage }); - if (data) device.queue.writeBuffer(buffer, 0, data); - return buffer; -}; - export default function get_renderer( pc: PointCloud, device: GPUDevice, presentation_format: GPUTextureFormat, camera_buffer: GPUBuffer, ): GaussianRenderer { - const sorter = get_sorter(pc.num_points, device); - + // =============================================== // Initialize GPU Buffers // =============================================== - const nulling_data = new Uint32Array([0]); + const renderSettingsBuffer = device.createBuffer({ + label: 'render settings', + size: 4 * 2, // 2x f32 + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, + }); + const renderSettingsData = new Float32Array(2); + const updateRenderSettings = (settings: { gaussianScaling?: number; shDeg?: number }) => { + renderSettingsData.set([ + settings?.gaussianScaling ?? renderSettingsData[0], + settings?.shDeg ?? renderSettingsData[1], + ]); + device.queue.writeBuffer(renderSettingsBuffer, 0, renderSettingsData); + }; + updateRenderSettings({ gaussianScaling: 1.0, shDeg: pc.sh_deg }); + + const splatByteSize = 4 * 5; // 5x u32 + const splatsBuffer = device.createBuffer({ + label: 'splats', + size: pc.num_points * splatByteSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }); + + // BELOW: JUST FOR RENDER PIPELINE + + const indirectDrawBuffer = device.createBuffer({ + label: 'indirect draw', + size: 4 * 4, // 4x u32 + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT, + }); + // we do 2 tris => 6 verts, instanced num_points times + const indirectDrawData = new Uint32Array([6, pc.num_points, 0, 0]); + device.queue.writeBuffer(indirectDrawBuffer, 0, indirectDrawData); + + // =============================================== + // Create Bind Group Layouts + // =============================================== + + const cameraUniformsBindGroupLayout = device.createBindGroupLayout({ + label: 'camera uniforms layout', + entries: [ + { + binding: 0, // CameraUniforms + visibility: GPUShaderStage.COMPUTE | GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: 'uniform' }, + }, + { + binding: 1, // RenderSettings + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'uniform' }, + }, + ], + }); + + const gaussianBindGroupLayout = device.createBindGroupLayout({ + label: 'gaussians and splats layout', + entries: [ + { + binding: 0, // gaussians + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'read-only-storage' }, + }, + { + binding: 1, // sh coefficients + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'read-only-storage' }, + }, + { + binding: 2, // splats + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'storage' }, + }, + ], + }); + + const sorterBindGroupLayout = device.createBindGroupLayout({ + label: 'sorting bind group layout', + entries: [...Array(4).keys()].map((idx) => ({ + binding: idx, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'storage' }, + })), + }); + + const splatsBindGroupLayout = device.createBindGroupLayout({ + label: 'splats bind group layout', + entries: [ + { + binding: 0, // splats + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: 'read-only-storage' }, + }, + { + binding: 1, // splat indices + visibility: GPUShaderStage.VERTEX, + buffer: { type: 'read-only-storage' }, + }, + ], + }); // =============================================== // Create Compute Pipeline and Bind Groups // =============================================== + + const preprocessPipelineLayout = device.createPipelineLayout({ + label: 'gaussian preprocess layout', + bindGroupLayouts: [ + cameraUniformsBindGroupLayout, + gaussianBindGroupLayout, + sorterBindGroupLayout, + ], + }); + const preprocess_pipeline = device.createComputePipeline({ label: 'preprocess', - layout: 'auto', + layout: preprocessPipelineLayout, compute: { module: device.createShaderModule({ code: preprocessWGSL }), entryPoint: 'preprocess', @@ -52,9 +145,28 @@ export default function get_renderer( }, }); - const sort_bind_group = device.createBindGroup({ + const cameraUniformsBindGroup = device.createBindGroup({ + label: 'camera uniforms', + layout: cameraUniformsBindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: camera_buffer } }, + { binding: 1, resource: { buffer: renderSettingsBuffer } }, + ], + }); + + const gaussianBindGroup = device.createBindGroup({ + label: 'gaussians and splats', + layout: gaussianBindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: pc.gaussian_3d_buffer } }, + { binding: 1, resource: { buffer: pc.sh_buffer } }, + { binding: 2, resource: { buffer: splatsBuffer } }, + ], + }); + + const sortBindGroup = device.createBindGroup({ label: 'sort', - layout: preprocess_pipeline.getBindGroupLayout(2), + layout: sorterBindGroupLayout, entries: [ { binding: 0, resource: { buffer: sorter.sort_info_buffer } }, { binding: 1, resource: { buffer: sorter.ping_pong[0].sort_depths_buffer } }, @@ -63,24 +175,122 @@ export default function get_renderer( ], }); - // =============================================== // Create Render Pipeline and Bind Groups // =============================================== - + + const renderPipelineLayout = device.createPipelineLayout({ + label: 'gaussian render layout', + bindGroupLayouts: [splatsBindGroupLayout, cameraUniformsBindGroupLayout], + }); + + const renderShaderModule = device.createShaderModule({ + label: 'gaussian render module', + code: renderWGSL, + }); + + const renderPipeline = device.createRenderPipeline({ + label: 'gaussian render pipeline', + layout: renderPipelineLayout, + vertex: { + module: renderShaderModule, + entryPoint: 'vs_main', + }, + fragment: { + module: renderShaderModule, + entryPoint: 'fs_main', + targets: [ + { + format: presentation_format, + blend: { + color: { + srcFactor: 'src-alpha', + dstFactor: 'one-minus-src-alpha', + operation: 'add', + }, + alpha: { + srcFactor: 'one', + dstFactor: 'one-minus-src-alpha', + operation: 'add', + }, + }, + }, + ], + }, + primitive: { + topology: 'triangle-list', + cullMode: 'none', + frontFace: 'ccw', + }, + }); + + const splatsBindGroup = device.createBindGroup({ + label: 'splats bind group', + layout: splatsBindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: splatsBuffer } }, + { binding: 1, resource: { buffer: sorter.ping_pong[0].sort_indices_buffer } }, + ], + }); // =============================================== // Command Encoder Functions // =============================================== - + + const preprocess = (encoder: GPUCommandEncoder) => { + const computePass = encoder.beginComputePass({ + label: 'gaussian preprocess', + }); + computePass.setPipeline(preprocess_pipeline); + computePass.setBindGroup(0, cameraUniformsBindGroup); + computePass.setBindGroup(1, gaussianBindGroup); + computePass.setBindGroup(2, sortBindGroup); + + const workgroupCount = Math.ceil(pc.num_points / C.histogram_wg_size); + computePass.dispatchWorkgroups(workgroupCount); + + computePass.end(); + }; + + const render = (encoder: GPUCommandEncoder, view: GPUTextureView) => { + const renderPass = encoder.beginRenderPass({ + label: 'gaussian render', + colorAttachments: [ + { + view, + clearValue: [0, 0, 0, 1], + loadOp: 'clear', + storeOp: 'store', + }, + ], + // no need for depth buffer, we sort everything anyway + }); + renderPass.setPipeline(renderPipeline); + renderPass.setBindGroup(0, splatsBindGroup); + renderPass.setBindGroup(1, cameraUniformsBindGroup); + + renderPass.drawIndirect(indirectDrawBuffer, 0); + + renderPass.end(); + }; // =============================================== // Return Render Object // =============================================== return { frame: (encoder: GPUCommandEncoder, texture_view: GPUTextureView) => { + // run preprocess compute pipeline + sorter.reset(encoder); + preprocess(encoder); + sorter.sort(encoder); + // copy number of instances from sorting info + encoder.copyBufferToBuffer(sorter.sort_info_buffer, 0, indirectDrawBuffer, 4, 4); + + // run indirect rendering pipeline + render(encoder, texture_view); }, camera_buffer, + updateRenderSettings, }; } diff --git a/src/renderers/point-cloud-renderer.ts b/src/renderers/point-cloud-renderer.ts index 36a2e8e..62daf90 100644 --- a/src/renderers/point-cloud-renderer.ts +++ b/src/renderers/point-cloud-renderer.ts @@ -1,13 +1,14 @@ import { PointCloud } from '../utils/load'; -import pointcloud_wgsl from '../shaders/point_cloud.wgsl'; +import pointcloud_wgsl from '../shaders/point_cloud.wgsl?raw'; import { Renderer } from './renderer'; export default function get_renderer( pc: PointCloud, device: GPUDevice, presentation_format: GPUTextureFormat, - camera_buffer: GPUBuffer): Renderer { - const render_shader = device.createShaderModule({code: pointcloud_wgsl}); + camera_buffer: GPUBuffer, +): Renderer { + const render_shader = device.createShaderModule({ code: pointcloud_wgsl }); const render_pipeline = device.createRenderPipeline({ label: 'render', layout: 'auto', @@ -28,15 +29,13 @@ export default function get_renderer( const camera_bind_group = device.createBindGroup({ label: 'point cloud camera', layout: render_pipeline.getBindGroupLayout(0), - entries: [{binding: 0, resource: { buffer: camera_buffer }}], + entries: [{ binding: 0, resource: { buffer: camera_buffer } }], }); const gaussian_bind_group = device.createBindGroup({ label: 'point cloud gaussians', layout: render_pipeline.getBindGroupLayout(1), - entries: [ - {binding: 0, resource: { buffer: pc.gaussian_3d_buffer }}, - ], + entries: [{ binding: 0, resource: { buffer: pc.gaussian_3d_buffer } }], }); const render = (encoder: GPUCommandEncoder, texture_view: GPUTextureView) => { @@ -46,8 +45,9 @@ export default function get_renderer( { view: texture_view, loadOp: 'clear', + clearValue: [0, 0, 0, 1], storeOp: 'store', - } + }, ], }); pass.setPipeline(render_pipeline); @@ -65,4 +65,4 @@ export default function get_renderer( camera_buffer, }; -} \ No newline at end of file +} diff --git a/src/renderers/renderer.ts b/src/renderers/renderer.ts index ffdf9ba..4495063 100644 --- a/src/renderers/renderer.ts +++ b/src/renderers/renderer.ts @@ -3,29 +3,30 @@ import { Pane } from 'tweakpane'; import * as TweakpaneFileImportPlugin from 'tweakpane-plugin-file-import'; import { default as get_renderer_gaussian, GaussianRenderer } from './gaussian-renderer'; import { default as get_renderer_pointcloud } from './point-cloud-renderer'; -import { Camera, load_camera_presets} from '../camera/camera'; +import { Camera, load_camera_presets } from '../camera/camera'; import { CameraControl } from '../camera/camera-control'; import { time, timeReturn } from '../utils/simple-console'; export interface Renderer { - frame: (encoder: GPUCommandEncoder, texture_view: GPUTextureView) => void, - camera_buffer: GPUBuffer, + frame: (encoder: GPUCommandEncoder, texture_view: GPUTextureView) => void; + camera_buffer: GPUBuffer; } export default async function init( canvas: HTMLCanvasElement, context: GPUCanvasContext, - device: GPUDevice + device: GPUDevice, ) { - let ply_file_loaded = false; - let cam_file_loaded = false; - let renderers: { pointcloud?: Renderer, gaussian?: Renderer } = {}; - let gaussian_renderer: GaussianRenderer | undefined; - let pointcloud_renderer: Renderer | undefined; - let renderer: Renderer | undefined; + let ply_file_loaded = false; + let cam_file_loaded = false; + let renderers: { pointcloud?: Renderer; gaussian?: Renderer } = {}; + let gaussian_renderer: GaussianRenderer | undefined; + let pointcloud_renderer: Renderer | undefined; + let renderer: Renderer | undefined; let cameras; - + const camera = new Camera(canvas, device); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const control = new CameraControl(camera); const observer = new ResizeObserver(() => { @@ -35,14 +36,13 @@ export default async function init( camera.on_update_canvas(); }); observer.observe(canvas); - + const presentation_format = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device, format: presentation_format, alphaMode: 'opaque', }); - // Tweakpane: easily adding tweak control for parameters. const params = { @@ -60,73 +60,83 @@ export default async function init( pane.registerPlugin(TweakpaneFileImportPlugin); { pane.addMonitor(params, 'fps', { - readonly:true + readonly: true, }); } { - pane.addInput(params, 'renderer', { - options: { - pointcloud: 'pointcloud', - gaussian: 'gaussian', - } - }).on('change', (e) => { - renderer = renderers[e.value]; - }); + pane + .addInput(params, 'renderer', { + options: { + pointcloud: 'pointcloud', + gaussian: 'gaussian', + }, + }) + .on('change', (e) => { + renderer = renderers[e.value]; + }); } { - pane.addInput(params, 'ply_file', { - view: 'file-input', - lineCount: 3, - filetypes: ['.ply'], - invalidFiletypeMessage: "We can't accept those filetypes!" - }) - .on('change', async (file) => { - const uploadedFile = file.value; - if (uploadedFile) { - const pc = await load(uploadedFile, device); - pointcloud_renderer = get_renderer_pointcloud(pc, device, presentation_format, camera.uniform_buffer); - gaussian_renderer = get_renderer_gaussian(pc, device, presentation_format, camera.uniform_buffer); - renderers = { - pointcloud: pointcloud_renderer, - gaussian: gaussian_renderer, - }; - renderer = renderers[params.renderer]; - ply_file_loaded = true; - }else{ - ply_file_loaded = false; - } - }); + pane + .addInput(params, 'ply_file', { + view: 'file-input', + lineCount: 3, + filetypes: ['.ply'], + invalidFiletypeMessage: "We can't accept those filetypes!", + }) + .on('change', async (file) => { + const uploadedFile = file.value; + if (uploadedFile) { + const pc = await load(uploadedFile, device); + pointcloud_renderer = get_renderer_pointcloud( + pc, + device, + presentation_format, + camera.uniform_buffer, + ); + gaussian_renderer = get_renderer_gaussian( + pc, + device, + presentation_format, + camera.uniform_buffer, + ); + renderers = { + pointcloud: pointcloud_renderer, + gaussian: gaussian_renderer, + }; + renderer = renderers[params.renderer]; + ply_file_loaded = true; + } else { + ply_file_loaded = false; + } + }); } { - pane.addInput(params, 'cam_file', { - view: 'file-input', - lineCount: 3, - filetypes: ['.json'], - invalidFiletypeMessage: "We can't accept those filetypes!" - }) - .on('change', async (file) => { - const uploadedFile = file.value; - if (uploadedFile) { - cameras=await load_camera_presets(file.value); - camera.set_preset(cameras[0]); - cam_file_loaded = true; - }else{ - cam_file_loaded = false; - } - }); + pane + .addInput(params, 'cam_file', { + view: 'file-input', + lineCount: 3, + filetypes: ['.json'], + invalidFiletypeMessage: "We can't accept those filetypes!", + }) + .on('change', async (file) => { + const uploadedFile = file.value; + if (uploadedFile) { + cameras = await load_camera_presets(file.value); + camera.set_preset(cameras[0]); + cam_file_loaded = true; + } else { + cam_file_loaded = false; + } + }); } { - pane.addInput( - params, - 'gaussian_multiplier', - {min: 0, max: 1.5} - ).on('change', (e) => { - //TODO: Bind constants to the gaussian renderer. + pane.addInput(params, 'gaussian_multiplier', { min: 0, max: 1.5 }).on('change', (evt) => { + gaussian_renderer?.updateRenderSettings({ gaussianScaling: evt.value }); }); } document.addEventListener('keydown', (event) => { - switch(event.key) { + switch (event.key) { case '0': case '1': case '2': @@ -136,17 +146,18 @@ export default async function init( case '6': case '7': case '8': - case '9': + case '9': { const i = parseInt(event.key); console.log(`set to camera preset ${i}`); camera.set_preset(cameras[i]); break; + } } }); function frame() { if (ply_file_loaded && cam_file_loaded) { - params.fps=1.0/timeReturn()*1000.0; + params.fps = (1.0 / timeReturn()) * 1000.0; time(); const encoder = device.createCommandEncoder(); const texture_view = context.getCurrentTexture().createView(); diff --git a/src/shaders/gaussian.wgsl b/src/shaders/gaussian.wgsl index 759226d..186c77d 100644 --- a/src/shaders/gaussian.wgsl +++ b/src/shaders/gaussian.wgsl @@ -1,22 +1,83 @@ +struct Splat { + position: u32, // 2x f16 + color: array, // 3x f16 rgb, 1x f16 opacity + conic: array, // 3x f16 cov, 1x f16 radius +}; + +struct CameraUniforms { + view: mat4x4, + view_inv: mat4x4, + proj: mat4x4, + proj_inv: mat4x4, + viewport: vec2, + focal: vec2 +}; + +@group(0) @binding(0) var splats: array; +@group(0) @binding(1) var splat_indices: array; + +@group(1) @binding(0) var camera: CameraUniforms; +// can also grab render settings if needed + struct VertexOutput { @builtin(position) position: vec4, - //TODO: information passed from vertex shader to fragment shader + @location(0) color: vec4, + @location(1) splat_center: vec2, + @location(2) conic: vec3, }; -struct Splat { - //TODO: information defined in preprocess compute shader -}; +const VERT_OFFSETS = array( + vec2f(-1., 1.), + vec2f(1., 1.), + vec2f(-1., -1.), + vec2f(1., -1.), + vec2f(-1., -1.), + vec2f(1., 1.), +); @vertex fn vs_main( + @builtin(vertex_index) vert_idx: u32, + @builtin(instance_index) instance_idx: u32, ) -> VertexOutput { - //TODO: reconstruct 2D quad based on information from splat, pass + let splat_idx = splat_indices[instance_idx]; + let splat = splats[splat_idx]; + var out: VertexOutput; - out.position = vec4(1. ,1. , 0., 1.); + + // unpack pos + let center_pos = unpack2x16float(splat.position); + // give it to fragment shader in pixel space + out.splat_center = (center_pos * vec2f(0.5, -0.5) + 0.5) * camera.viewport; + + // unpack color and opacity + out.color = vec4f(unpack2x16float(splat.color[0]), unpack2x16float(splat.color[1])); + + // unpack conic and radius + let conic_xy = unpack2x16float(splat.conic[0]); + let conic_z_radius = unpack2x16float(splat.conic[1]); + out.conic = vec3f(conic_xy, conic_z_radius.x); + let radius = conic_z_radius.y; + + // set vert pos based on radius + let vert_pos_offset: vec2f = VERT_OFFSETS[vert_idx] * radius; + let vert_pos = center_pos + vert_pos_offset / camera.viewport; + out.position = vec4(vert_pos, 0., 1.); + return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(1.); -} \ No newline at end of file + // convert to NDC, get distance + let d = in.position.xy - in.splat_center; + + // use insane conic matrix + let power = -0.5 * (in.conic.x * d.x * d.x + in.conic.z * d.y * d.y) - in.conic.y * d.x * d.y; + if power > 0.0 { + discard; + } + let alpha = min(0.99, in.color.a * exp(power)); + + return vec4f(in.color.rgb, alpha); +} diff --git a/src/shaders/point_cloud.wgsl b/src/shaders/point_cloud.wgsl index 01dded1..5be80ab 100644 --- a/src/shaders/point_cloud.wgsl +++ b/src/shaders/point_cloud.wgsl @@ -34,8 +34,7 @@ fn vs_main( let b = unpack2x16float(vertex.pos_opacity[1]); let pos = vec4(a.x, a.y, b.x, 1.); - // TODO: MVP calculations - out.position = pos; + out.position = camera.proj * camera.view * pos; return out; } @@ -43,4 +42,4 @@ fn vs_main( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { return vec4(1., 1., 0., 1.); -} \ No newline at end of file +} diff --git a/src/shaders/preprocess.wgsl b/src/shaders/preprocess.wgsl index bbc63f5..212c765 100644 --- a/src/shaders/preprocess.wgsl +++ b/src/shaders/preprocess.wgsl @@ -28,8 +28,8 @@ struct DispatchIndirect { struct SortInfos { keys_size: atomic, // instance_count in DrawIndirect - //data below is for info inside radix sort - padded_size: u32, + //data below is for info inside radix sort + padded_size: u32, passes: u32, even_pass: u32, odd_pass: u32, @@ -56,23 +56,36 @@ struct Gaussian { }; struct Splat { - //TODO: store information for 2D splat rendering + position: u32, // 2x f16 + color: array, // 3x f16 rgb, 1x f16 opacity + conic: array, // 3x f16 cov, 1x f16 radius }; -//TODO: bind your data here -@group(2) @binding(0) -var sort_infos: SortInfos; -@group(2) @binding(1) -var sort_depths : array; -@group(2) @binding(2) -var sort_indices : array; -@group(2) @binding(3) -var sort_dispatch: DispatchIndirect; - -/// reads the ith sh coef from the storage buffer +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var render_settings: RenderSettings; + +@group(1) @binding(0) var gaussians: array; +@group(1) @binding(1) var sh_coefficients: array; +@group(1) @binding(2) var splats: array; + +@group(2) @binding(0) var sort_infos: SortInfos; +@group(2) @binding(1) var sort_depths: array; +@group(2) @binding(2) var sort_indices: array; +@group(2) @binding(3) var sort_dispatch: DispatchIndirect; + +/// reads the ith sh coef from the storage buffer fn sh_coef(splat_idx: u32, c_idx: u32) -> vec3 { - //TODO: access your binded sh_coeff, see load.ts for how it is stored - return vec3(0.0); + let channel = c_idx & 1u; + let start_idx = splat_idx * 24 + (c_idx / 2) * 3 + channel; + + let col_01 = unpack2x16float(sh_coefficients[start_idx]); + let col_23 = unpack2x16float(sh_coefficients[start_idx + 1]); + + if channel == 0u { + return vec3f(col_01.xy, col_23.x); + } else { + return vec3f(col_01.y, col_23.xy); + } } // spherical harmonics evaluation with Condon–Shortley phase @@ -109,10 +122,127 @@ fn computeColorFromSH(dir: vec3, v_idx: u32, sh_deg: u32) -> vec3 { } @compute @workgroup_size(workgroupSize,1,1) -fn preprocess(@builtin(global_invocation_id) gid: vec3, @builtin(num_workgroups) wgs: vec3) { +fn preprocess( + @builtin(global_invocation_id) gid: vec3, + @builtin(num_workgroups) wgs: vec3 +) { let idx = gid.x; - //TODO: set up pipeline as described in instruction + if idx >= arrayLength(&gaussians) { + return; + } + + let gaussian = gaussians[idx]; - let keys_per_dispatch = workgroupSize * sortKeyPerThread; + // unpack position and opacity + let pos_xy = unpack2x16float(gaussian.pos_opacity[0]); + let pos_z_opacity = unpack2x16float(gaussian.pos_opacity[1]); + let pos = vec4f(pos_xy, pos_z_opacity.x, 1.); + + var pos_view = camera.view * pos; + var pos_ndc = camera.proj * pos_view; + pos_ndc /= pos_ndc.w; + + // simple frustum culling + if any(abs(pos_ndc.xy) > vec2f(1.2)) || pos_ndc.z < 0. { + return; + } + + // unpack rot and scale + let rot_wx = unpack2x16float(gaussian.rot[0]); + let rot_yz = unpack2x16float(gaussian.rot[1]); + let rot = vec4f(rot_wx.y, rot_yz.x, rot_yz.y, rot_wx.x); + + let scale_xy = exp(unpack2x16float(gaussian.scale[0])); + let scale_z_padding = exp(unpack2x16float(gaussian.scale[1])); + let scale = vec3f(scale_xy, scale_z_padding.x); + + // compute conic values + let R = mat3x3f( + 1. - 2. * (rot.y * rot.y + rot.z * rot.z), 2. * (rot.x * rot.y - rot.w * rot.z), 2. * (rot.x * rot.z + rot.w * rot.y), + 2. * (rot.x * rot.y + rot.w * rot.z), 1. - 2. * (rot.x * rot.x + rot.z * rot.z), 2. * (rot.y * rot.z - rot.w * rot.x), + 2. * (rot.x * rot.z - rot.w * rot.y), 2. * (rot.y * rot.z + rot.w * rot.x), 1. - 2. * (rot.x * rot.x + rot.y * rot.y), + ); + var S = mat3x3f( + render_settings.gaussian_scaling * scale.x, 0., 0., + 0., render_settings.gaussian_scaling * scale.y, 0., + 0., 0., render_settings.gaussian_scaling * scale.z, + ); + + let cov3D = transpose(S * R) * S * R; + + let t = pos_view.xyz; + let J = mat3x3f( + camera.focal.x / t.z, 0.0, -(camera.focal.x * t.x) / (t.z * t.z), + 0.0, camera.focal.y / t.z, -(camera.focal.y * t.y) / (t.z * t.z), + 0.0, 0.0, 0.0 + ); + + let W = transpose(mat3x3f( + camera.view[0].xyz, camera.view[1].xyz, camera.view[2].xyz + )); + + let T = W * J; + + let Vrk = mat3x3f( + cov3D[0][0], cov3D[0][1], cov3D[0][2], + cov3D[0][1], cov3D[1][1], cov3D[1][2], + cov3D[0][2], cov3D[1][2], cov3D[2][2] + ); + + var cov2D = transpose(T) * transpose(Vrk) * T; + + // this addition is for stability apparently? + cov2D[0][0] += 0.3; + cov2D[1][1] += 0.3; + + // compute conic and radius + let cov = vec3f(cov2D[0][0], cov2D[0][1], cov2D[1][1]); + let det = cov.x * cov.z - cov.y * cov.y; + if det == 0.0 { + return; + } + + // compute radius + let mid = 0.5 * (cov.x + cov.z); + let lambda1 = mid + sqrt(max(0.1, mid * mid - det)); // The author is not too sure what 0.1 serves here :o + let lambda2 = mid - sqrt(max(0.1, mid * mid - det)); + let radius = ceil(3.0 * sqrt(max(lambda1, lambda2))); + + // compute conic + let inv_det = 1.0 / det; + let conic = vec3f(cov.z * inv_det, -cov.y * inv_det, cov.x * inv_det); + + // compute color and opacity + let cam_pos = -camera.view[3].xyz; + let camera_to_splat = normalize(pos.xyz - cam_pos); + let color = computeColorFromSH(camera_to_splat, idx, u32(render_settings.sh_deg)); + let opacity = 1.0 / (1.0 + exp(-pos_z_opacity.y)); + + // add to splat buffer atomically + let splat_idx = atomicAdd(&sort_infos.keys_size, 1u); + splats[splat_idx].position = pack2x16float(pos_ndc.xy); + splats[splat_idx].color[0] = pack2x16float(color.rg); + splats[splat_idx].color[1] = pack2x16float(vec2f(color.b, opacity)); + splats[splat_idx].conic[0] = pack2x16float(conic.xy); + splats[splat_idx].conic[1] = pack2x16float(vec2f(conic.z, radius)); + + // update sorting info! + sort_indices[splat_idx] = splat_idx; + + let depth = -pos_view.z; + let depth_bits = bitcast(depth); + + // Flip all bits if negative (sign bit set), otherwise flip just the sign bit + let sortable_depth = select( + depth_bits ^ 0x80000000u, // Positive: flip sign bit + ~depth_bits, // Negative: flip all bits + (depth_bits & 0x80000000u) != 0u + ); + sort_depths[splat_idx] = sortable_depth; + + let keys_per_dispatch = workgroupSize * sortKeyPerThread; // increment DispatchIndirect.dispatchx each time you reach limit for one dispatch of keys -} \ No newline at end of file + if splat_idx % keys_per_dispatch == 0u { + atomicAdd(&sort_dispatch.dispatch_x, 1u); + } // how the hell does this work +} diff --git a/src/sort/sort.ts b/src/sort/sort.ts index 4bbeb23..85c0ebc 100644 --- a/src/sort/sort.ts +++ b/src/sort/sort.ts @@ -8,22 +8,22 @@ All shaders can be found in shaders/radix_sort.wgsl */ -import radix_sort_wgsl from './radix_sort.wgsl'; +import radix_sort_wgsl from './radix_sort.wgsl?raw'; import { align } from '../utils/util'; export interface SortStuff { - sort: (encoder: GPUCommandEncoder) => void, - sort_info_buffer: GPUBuffer, - sort_dispatch_indirect_buffer: GPUBuffer, + sort: (encoder: GPUCommandEncoder) => void; + reset: (encoder: GPUCommandEncoder) => void; + sort_info_buffer: GPUBuffer; + sort_dispatch_indirect_buffer: GPUBuffer; // ping-pong ping_pong: { - sort_indices_buffer: GPUBuffer, - sort_depths_buffer: GPUBuffer, - }[] + sort_indices_buffer: GPUBuffer; + sort_depths_buffer: GPUBuffer; + }[]; } - function create_ping_pong_buffer(adjusted_count: number, keysize: number, device: GPUDevice) { return { // payload @@ -69,7 +69,7 @@ function create_pipelines(device: GPUDevice) { // code: radix_sort_wgsl, code: `const rs_mem_dwords = ${C.rs_mem_dwords}u; ${radix_sort_wgsl} - ` + `, }); const bind_group_layout = device.createBindGroupLayout({ @@ -80,7 +80,7 @@ function create_pipelines(device: GPUDevice) { visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage', - } + }, }, // histograms { @@ -88,7 +88,7 @@ function create_pipelines(device: GPUDevice) { visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage', - } + }, }, // keys_a { @@ -96,7 +96,7 @@ function create_pipelines(device: GPUDevice) { visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage', - } + }, }, // keys_b { @@ -104,7 +104,7 @@ function create_pipelines(device: GPUDevice) { visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage', - } + }, }, // payload_a { @@ -112,7 +112,7 @@ function create_pipelines(device: GPUDevice) { visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage', - } + }, }, // payload_b { @@ -120,9 +120,9 @@ function create_pipelines(device: GPUDevice) { visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage', - } + }, }, - ] + ], }); const pipeline_layout = device.createPipelineLayout({ @@ -144,7 +144,7 @@ function create_pipelines(device: GPUDevice) { rs_radix_log2: C.rs_radix_log2, rs_radix_size: C.rs_radix_size, rs_keyval_size: C.rs_keyval_size, - } + }, }, }), histogram: device.createComputePipeline({ @@ -152,7 +152,7 @@ function create_pipelines(device: GPUDevice) { compute: { module: module, entryPoint: 'calculate_histogram', - } + }, }), prefix: device.createComputePipeline({ layout: pipeline_layout, @@ -162,8 +162,8 @@ function create_pipelines(device: GPUDevice) { constants: { rs_radix_size: C.histogram_wg_size, prefix_wg_size: C.prefix_wg_size, - } - } + }, + }, }), scatter_odd: device.createComputePipeline({ layout: pipeline_layout, @@ -177,8 +177,8 @@ function create_pipelines(device: GPUDevice) { rs_radix_size: C.rs_radix_size, rs_keyval_size: C.rs_keyval_size, scatter_wg_size: C.scatter_wg_size, - } - } + }, + }, }), scatter_even: device.createComputePipeline({ layout: pipeline_layout, @@ -192,11 +192,11 @@ function create_pipelines(device: GPUDevice) { rs_radix_size: C.rs_radix_size, rs_keyval_size: C.rs_keyval_size, scatter_wg_size: C.scatter_wg_size, - } - } + }, + }, }), }; -}; +} function get_scatter_histogram_sizes(keysize: number) { // as a general rule of thumb, scater_blocks_ru is equal to histo_blocks_ru, except the amount of elements in these two stages is different @@ -210,12 +210,12 @@ function get_scatter_histogram_sizes(keysize: number) { const count_ru_histo = histo_blocks_ru * histo_block_kvs; return { - scatter_block_kvs, - scatter_blocks_ru, - count_ru_scatter, - histo_block_kvs, - histo_blocks_ru, - count_ru_histo, + scatter_block_kvs, + scatter_blocks_ru, + count_ru_scatter, + histo_block_kvs, + histo_blocks_ru, + count_ru_histo, }; } @@ -227,9 +227,11 @@ function create_histogram_buffer(keysize: number, device: GPUDevice) { // subgroup and workgroup sizes const histo_sg_size = C.histogram_sg_size; + /* eslint-disable @typescript-eslint/no-unused-vars */ const _histo_wg_size = C.histogram_wg_size; const _prefix_sg_size = histo_sg_size; const _internal_sg_size = histo_sg_size; + /* eslint-enable @typescript-eslint/no-unused-vars */ // The "internal" memory map looks like this: // +---------------------------------+ <-- 0 @@ -257,11 +259,20 @@ const num_pass = 4; export function get_sorter(keysize: number, device: GPUDevice): SortStuff { const keys_per_workgroup = C.histogram_wg_size * C.rs_histogram_block_rows; - const keys_count_adjusted = (Math.floor((keysize + keys_per_workgroup - 1) / keys_per_workgroup) + 1) * keys_per_workgroup; + const keys_count_adjusted = + (Math.floor((keysize + keys_per_workgroup - 1) / keys_per_workgroup) + 1) * keys_per_workgroup; console.log(`keys count adjusted: ${keys_count_adjusted}`); // histogram count console.log(`key size: ${keysize}`); + const nullBuffer = device.createBuffer({ + label: 'null buffer', + size: 4, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + const nulling_data = new Uint32Array([0]); + device.queue.writeBuffer(nullBuffer, 0, nulling_data); + const sort_info_buffer = device.createBuffer({ label: 'sort info', size: 5 * 4, @@ -284,8 +295,16 @@ export function get_sorter(keysize: number, device: GPUDevice): SortStuff { const histogram_buffer = create_histogram_buffer(keysize, device); const { scatter_blocks_ru, count_ru_histo } = get_scatter_histogram_sizes(keysize); - device.queue.writeBuffer(sort_info_buffer, 0, new Uint32Array([keysize, count_ru_histo, num_pass, 0, 0])); - device.queue.writeBuffer(sort_dispatch_indirect_buffer, 0, new Uint32Array([scatter_blocks_ru, 1, 1])); + device.queue.writeBuffer( + sort_info_buffer, + 0, + new Uint32Array([keysize, count_ru_histo, num_pass, 0, 0]), + ); + device.queue.writeBuffer( + sort_dispatch_indirect_buffer, + 0, + new Uint32Array([scatter_blocks_ru, 1, 1]), + ); const bind_group = device.createBindGroup({ label: 'sort', @@ -297,7 +316,7 @@ export function get_sorter(keysize: number, device: GPUDevice): SortStuff { { binding: 3, resource: { buffer: ping_pong[1].sort_depths_buffer } }, { binding: 4, resource: { buffer: ping_pong[0].sort_indices_buffer } }, { binding: 5, resource: { buffer: ping_pong[1].sort_indices_buffer } }, - ] + ], }); function record_calculate_histogram_indirect(encoder: GPUCommandEncoder) { @@ -338,7 +357,7 @@ export function get_sorter(keysize: number, device: GPUDevice): SortStuff { pass.setBindGroup(0, bind_group); // assert: passes == 4 - + pass.setPipeline(pipelines.scatter_even); pass.dispatchWorkgroupsIndirect(sort_dispatch_indirect_buffer, 0); pass.setPipeline(pipelines.scatter_odd); @@ -354,7 +373,12 @@ export function get_sorter(keysize: number, device: GPUDevice): SortStuff { record_calculate_histogram_indirect(encoder); record_prefix_histogram(encoder); record_scatter_keys_indirect(encoder); - }; + } + + function reset(encoder: GPUCommandEncoder) { + encoder.copyBufferToBuffer(nullBuffer, 0, sort_info_buffer, 0, 4); + encoder.copyBufferToBuffer(nullBuffer, 0, sort_dispatch_indirect_buffer, 0, 4); + } return { sort_info_buffer, @@ -362,5 +386,6 @@ export function get_sorter(keysize: number, device: GPUDevice): SortStuff { ping_pong, sort, + reset, }; } diff --git a/src/style.css b/src/style.css index f2cdc96..5573392 100644 --- a/src/style.css +++ b/src/style.css @@ -14,19 +14,30 @@ canvas { position: absolute; margin: 20px 20px; - & h1,h2,h3,h4 { - color:#000; - text-shadow: -1px -1px 0 #ccc, 1px -1px 0 #ccc, -1px 1px 0 #ccc, 1px 1px 0 #ccc; + & h1, + h2, + h3, + h4 { + color: #000; + text-shadow: + -1px -1px 0 #ccc, + 1px -1px 0 #ccc, + -1px 1px 0 #ccc, + 1px 1px 0 #ccc; margin: auto; } & #log { - color:#fff; - text-shadow: -1px -1px 0 #555, 1px -1px 0 #555, -1px 1px 0 #555, 1px 1px 0 #555; + color: #fff; + text-shadow: + -1px -1px 0 #555, + 1px -1px 0 #555, + -1px 1px 0 #555, + 1px 1px 0 #555; & p { font-size: 12px; margin: 0.2em; } } -} \ No newline at end of file +} diff --git a/src/types.d.ts b/src/types.d.ts index e6cb88c..7039597 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,4 +1,4 @@ -declare module '*.wgsl' { +declare module '*.wgsl?raw' { const shader: string; export default shader; -} \ No newline at end of file +} diff --git a/src/utils/load.ts b/src/utils/load.ts index 030b857..a5f6a68 100644 --- a/src/utils/load.ts +++ b/src/utils/load.ts @@ -1,57 +1,52 @@ import { Float16Array } from '@petamoriken/float16'; import { log, time, timeLog } from './simple-console'; -import { decodeHeader, readRawVertex ,nShCoeffs} from './plyreader'; +import { decodeHeader, readRawVertex, nShCoeffs } from './plyreader'; -const c_size_float = 2; // byte size of f16 +const c_size_float = 2; // byte size of f16 const c_size_3d_gaussian = - 3 * c_size_float // x y z (position) - + c_size_float // opacity - + 4 * c_size_float // rotation - + 4 * c_size_float //scale -; - + 3 * c_size_float + // x y z (position) + c_size_float + // opacity + 4 * c_size_float + // rotation + 4 * c_size_float; //scale export type PointCloud = Awaited>; export async function load(file: string, device: GPUDevice) { const blob = new Blob([file]); const arrayBuffer = await new Promise((resolve, reject) => { const reader = new FileReader(); - - reader.onload = function(event) { - resolve(event.target.result); // Resolve the promise with the ArrayBuffer + + reader.onload = function (event) { + resolve(event.target.result); // Resolve the promise with the ArrayBuffer }; - reader.onerror = reject; // Reject the promise in case of an error + reader.onerror = reject; // Reject the promise in case of an error reader.readAsArrayBuffer(blob); }); const [vertexCount, propertyTypes, vertexData] = decodeHeader(arrayBuffer as ArrayBuffer); // figure out the SH degree from the number of coefficients - var nRestCoeffs = 0; + let nRestCoeffs = 0; for (const propertyName in propertyTypes) { - if (propertyName.startsWith('f_rest_')) { - nRestCoeffs += 1; - } + if (propertyName.startsWith('f_rest_')) { + nRestCoeffs += 1; + } } const nCoeffsPerColor = nRestCoeffs / 3; const sh_deg = Math.sqrt(nCoeffsPerColor + 1) - 1; const num_coefs = nShCoeffs(sh_deg); const max_num_coefs = 16; - const c_size_sh_coef = - 3 * max_num_coefs * c_size_float // 3 channels (RGB) x 16 coefs - ; - + const c_size_sh_coef = 3 * max_num_coefs * c_size_float; // 3 channels (RGB) x 16 coefs // figure out the order in which spherical harmonics should be read const shFeatureOrder = []; for (let rgb = 0; rgb < 3; ++rgb) { - shFeatureOrder.push(`f_dc_${rgb}`); + shFeatureOrder.push(`f_dc_${rgb}`); } for (let i = 0; i < nCoeffsPerColor; ++i) { - for (let rgb = 0; rgb < 3; ++rgb) { - shFeatureOrder.push(`f_rest_${rgb * nCoeffsPerColor + i}`); - } + for (let rgb = 0; rgb < 3; ++rgb) { + shFeatureOrder.push(`f_rest_${rgb * nCoeffsPerColor + i}`); + } } const num_points = vertexCount; @@ -63,7 +58,7 @@ export async function load(file: string, device: GPUDevice) { // xyz (position), opacity, cov (from rot and scale) const gaussian_3d_buffer = device.createBuffer({ label: 'ply input 3d gaussians data buffer', - size: num_points * c_size_3d_gaussian, // buffer size multiple of 4? + size: num_points * c_size_3d_gaussian, // buffer size multiple of 4? usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, mappedAtCreation: true, }); @@ -72,26 +67,26 @@ export async function load(file: string, device: GPUDevice) { // Spherical harmonic function coeffs const sh_buffer = device.createBuffer({ label: 'ply input 3d gaussians data buffer', - size: num_points * c_size_sh_coef, // buffer size multiple of 4? + size: num_points * c_size_sh_coef, // buffer size multiple of 4? usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, mappedAtCreation: true, }); const sh = new Float16Array(sh_buffer.getMappedRange()); - var readOffset = 0; + let readOffset = 0; for (let i = 0; i < num_points; i++) { const [newReadOffset, rawVertex] = readRawVertex(readOffset, vertexData, propertyTypes); readOffset = newReadOffset; const o = i * (c_size_3d_gaussian / c_size_float); const output_offset = i * max_num_coefs * 3; - + for (let order = 0; order < num_coefs; ++order) { - const order_offset = order * 3; - for (let j = 0; j < 3; ++j) { - const coeffName = shFeatureOrder[order * 3 + j]; - sh[output_offset +order_offset+j]=rawVertex[coeffName]; - } + const order_offset = order * 3; + for (let j = 0; j < 3; ++j) { + const coeffName = shFeatureOrder[order * 3 + j]; + sh[output_offset + order_offset + j] = rawVertex[coeffName]; + } } gaussian[o + 0] = rawVertex.x; @@ -107,11 +102,11 @@ export async function load(file: string, device: GPUDevice) { gaussian[o + 10] = rawVertex.scale_2; } - gaussian_3d_buffer.unmap(); + gaussian_3d_buffer.unmap(); sh_buffer.unmap(); timeLog(); - console.log("return result!"); + console.log('return result!'); return { num_points: num_points, sh_deg: sh_deg, diff --git a/src/utils/plyreader.ts b/src/utils/plyreader.ts index b965b9c..de8478a 100644 --- a/src/utils/plyreader.ts +++ b/src/utils/plyreader.ts @@ -1,90 +1,92 @@ -export function decodeHeader(plyArrayBuffer: ArrayBuffer): [number, Record, DataView] { - /* decodes the .ply file header and returns a tuple of: - * - vertexCount: number of vertices in the point cloud - * - propertyTypes: a map from property names to their types - * - vertexData: a DataView of the vertex data - */ +export function decodeHeader( + plyArrayBuffer: ArrayBuffer, +): [number, Record, DataView] { + /* decodes the .ply file header and returns a tuple of: + * - vertexCount: number of vertices in the point cloud + * - propertyTypes: a map from property names to their types + * - vertexData: a DataView of the vertex data + */ - const decoder = new TextDecoder(); - let headerOffset = 0; - let headerText = ''; + const decoder = new TextDecoder(); + let headerOffset = 0; + let headerText = ''; - while (true) { - const headerChunk = new Uint8Array(plyArrayBuffer, headerOffset, 50); - headerText += decoder.decode(headerChunk); - headerOffset += 50; + while (true) { + const headerChunk = new Uint8Array(plyArrayBuffer, headerOffset, 50); + headerText += decoder.decode(headerChunk); + headerOffset += 50; - if (headerText.includes('end_header')) { - break; - } + if (headerText.includes('end_header')) { + break; } + } - const headerLines = headerText.split('\n'); + const headerLines = headerText.split('\n'); - let vertexCount = 0; - let propertyTypes: Record = {}; + let vertexCount = 0; + const propertyTypes: Record = {}; - for (let i = 0; i < headerLines.length; i++) { - const line = headerLines[i].trim(); - if (line.startsWith('element vertex')) { - const vertexCountMatch = line.match(/\d+/); - if (vertexCountMatch) { - vertexCount = parseInt(vertexCountMatch[0]); - } - } else if (line.startsWith('property')) { - const propertyMatch = line.match(/(\w+)\s+(\w+)\s+(\w+)/); - if (propertyMatch) { - const propertyType = propertyMatch[2]; - const propertyName = propertyMatch[3]; - propertyTypes[propertyName] = propertyType; - } - } else if (line === 'end_header') { - break; - } + for (let i = 0; i < headerLines.length; i++) { + const line = headerLines[i].trim(); + if (line.startsWith('element vertex')) { + const vertexCountMatch = line.match(/\d+/); + if (vertexCountMatch) { + vertexCount = parseInt(vertexCountMatch[0]); + } + } else if (line.startsWith('property')) { + const propertyMatch = line.match(/(\w+)\s+(\w+)\s+(\w+)/); + if (propertyMatch) { + const propertyType = propertyMatch[2]; + const propertyName = propertyMatch[3]; + propertyTypes[propertyName] = propertyType; + } + } else if (line === 'end_header') { + break; } + } - const vertexByteOffset = headerText.indexOf('end_header') + 'end_header'.length + 1; - const vertexData = new DataView(plyArrayBuffer, vertexByteOffset); + const vertexByteOffset = headerText.indexOf('end_header') + 'end_header'.length + 1; + const vertexData = new DataView(plyArrayBuffer, vertexByteOffset); - return [ - vertexCount, - propertyTypes, - vertexData, - ]; + return [vertexCount, propertyTypes, vertexData]; } -export function readRawVertex(offset: number, vertexData: DataView, propertyTypes: Record): [number, Record] { - /* reads a single vertex from the vertexData DataView and returns a tuple of: - * - offset: the offset of the next vertex in the vertexData DataView - * - rawVertex: a map from property names to their values - */ - let rawVertex: Record = {}; +export function readRawVertex( + offset: number, + vertexData: DataView, + propertyTypes: Record, +): [number, Record] { + /* reads a single vertex from the vertexData DataView and returns a tuple of: + * - offset: the offset of the next vertex in the vertexData DataView + * - rawVertex: a map from property names to their values + */ + const rawVertex: Record = {}; - for (const property in propertyTypes) { - const propertyType = propertyTypes[property]; - if (propertyType === 'float') { - rawVertex[property] = vertexData.getFloat32(offset, true); - offset += Float32Array.BYTES_PER_ELEMENT; - } else if (propertyType === 'uchar') { - rawVertex[property] = vertexData.getUint8(offset) / 255.0; - offset += Uint8Array.BYTES_PER_ELEMENT; - } + for (const property in propertyTypes) { + const propertyType = propertyTypes[property]; + if (propertyType === 'float') { + rawVertex[property] = vertexData.getFloat32(offset, true); + offset += Float32Array.BYTES_PER_ELEMENT; + } else if (propertyType === 'uchar') { + rawVertex[property] = vertexData.getUint8(offset) / 255.0; + offset += Uint8Array.BYTES_PER_ELEMENT; } + } - return [offset, rawVertex]; + return [offset, rawVertex]; } export function nShCoeffs(sphericalHarmonicsDegree: number): number { - /* returns the expected number of spherical harmonics coefficients */ - if (sphericalHarmonicsDegree === 0) { - return 1; - } else if (sphericalHarmonicsDegree === 1) { - return 4; - } else if (sphericalHarmonicsDegree === 2) { - return 9; - } else if (sphericalHarmonicsDegree === 3) { - return 16; - } else { - throw new Error(`Unsupported SH degree: ${sphericalHarmonicsDegree}`); - } -} \ No newline at end of file + /* returns the expected number of spherical harmonics coefficients */ + if (sphericalHarmonicsDegree === 0) { + return 1; + } else if (sphericalHarmonicsDegree === 1) { + return 4; + } else if (sphericalHarmonicsDegree === 2) { + return 9; + } else if (sphericalHarmonicsDegree === 3) { + return 16; + } else { + throw new Error(`Unsupported SH degree: ${sphericalHarmonicsDegree}`); + } +} diff --git a/src/utils/simple-console.ts b/src/utils/simple-console.ts index 213ece1..7294d0f 100644 --- a/src/utils/simple-console.ts +++ b/src/utils/simple-console.ts @@ -18,7 +18,7 @@ export function timeLog() { log(`${d.toFixed(0)} ms`); } -export function timeReturn(){ +export function timeReturn() { const d = performance.now() - t; return d; -} \ No newline at end of file +} diff --git a/src/utils/util.ts b/src/utils/util.ts index 7ad96a8..1990c6e 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -1,28 +1,25 @@ /** * Asserts `condition` is true. Otherwise, throws an `Error` with the provided message. */ -export function assert( - condition: boolean, - msg?: string | (() => string) - ): asserts condition { - if (!condition) { - throw new Error(msg && (typeof msg === 'string' ? msg : msg())); - } +export function assert(condition: boolean, msg?: string | (() => string)): asserts condition { + if (!condition) { + throw new Error(msg && (typeof msg === 'string' ? msg : msg())); } +} /** If the argument is an Error, throw it. Otherwise, pass it back. */ export function assertOK(value: Error | T): T { -if (value instanceof Error) { + if (value instanceof Error) { throw value; -} -return value; + } + return value; } /** * Assert this code is unreachable. Unconditionally throws an `Error`. */ export function unreachable(msg?: string): never { -throw new Error(msg); + throw new Error(msg); } /** Round `n` up to the next multiple of `alignment` (inclusive). */ @@ -30,4 +27,4 @@ export function align(n: number, alignment: number): number { assert(Number.isInteger(n) && n >= 0, 'n must be a non-negative integer'); assert(Number.isInteger(alignment) && alignment > 0, 'alignment must be a positive integer'); return Math.ceil(n / alignment) * alignment; -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index 8f60b5b..2f763e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,11 +3,7 @@ "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", - "lib": [ - "ES2020", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, "allowJs": true, /* Bundler mode */ @@ -20,16 +16,8 @@ "strict": false, "noFallthroughCasesInSwitch": true, /* types */ - "typeRoots": [ - "./node_modules/@webgpu/types", - "./node_modules/tweakpane/dist/type/" - ], + "typeRoots": ["./node_modules/@webgpu/types", "./node_modules/tweakpane/dist/type/"] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "**/node_modules/**", - "**/dist/**" - ] -} \ No newline at end of file + "include": ["src/**/*.ts"], + "exclude": ["**/node_modules/**", "**/dist/**"] +} diff --git a/vite.config.ts b/vite.config.ts index 8c4aaa5..30e60a2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,14 +1,14 @@ import rawPlugin from 'vite-raw-plugin'; -import { defineConfig } from 'vite' +import { defineConfig } from 'vite'; export default defineConfig({ - build: { - target: 'esnext' - }, - base: process.env.GITHUB_ACTIONS_BASE || undefined, - plugins: [ - rawPlugin({ - fileRegex: /\.wgsl$/, - }), - ], -}) + build: { + target: 'esnext', + }, + base: process.env.GITHUB_ACTIONS_BASE || undefined, + plugins: [ + rawPlugin({ + fileRegex: /\.wgsl$/, + }), + ], +});