diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ca74a57d3..6c66309eb 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -25,10 +25,20 @@ }, }, { - "label": "viewer server+esbuild", + "label": "webgl-worker", + "type": "shell", + "command": "node buildWorkers.mjs -w", + "problemMatcher": "$esbuild-watch", + "presentation": { + "reveal": "silent" + }, + }, + { + "label": "viewer server+esbuild+workers", "dependsOn": [ "viewer-server", - "viewer-esbuild" + "viewer-esbuild", + "webgl-worker" ], "dependsOrder": "parallel", } diff --git a/buildWorkers.mjs b/buildWorkers.mjs new file mode 100644 index 000000000..24e986309 --- /dev/null +++ b/buildWorkers.mjs @@ -0,0 +1,61 @@ +// main worker file intended for computing world geometry is built using prismarine-viewer/buildWorker.mjs +import { build, context } from 'esbuild' +import fs from 'fs' +import path from 'path' + +const watch = process.argv.includes('-w') + +const result = await (watch ? context : build)({ + bundle: true, + platform: 'browser', + entryPoints: ['prismarine-viewer/examples/webglRendererWorker.ts'], + outdir: 'prismarine-viewer/public/', + sourcemap: watch ? 'inline' : 'external', + minify: !watch, + treeShaking: true, + logLevel: 'info', + alias: { + 'three': './node_modules/three/src/Three.js', + events: 'events', // make explicit + buffer: 'buffer', + 'fs': 'browserfs/dist/shims/fs.js', + http: 'http-browserify', + perf_hooks: './src/perf_hooks_replacement.js', + crypto: './src/crypto.js', + stream: 'stream-browserify', + net: 'net-browserify', + assert: 'assert', + dns: './src/dns.js' + }, + inject: [ + './src/shims.js' + ], + plugins: [ + { + name: 'writeOutput', + setup (build) { + build.onEnd(({ outputFiles }) => { + for (const file of outputFiles) { + for (const dir of ['prismarine-viewer/public', 'dist']) { + const baseName = path.basename(file.path) + fs.writeFileSync(path.join(dir, baseName), file.contents) + } + } + }) + } + } + ], + loader: { + '.vert': 'text', + '.frag': 'text' + }, + mainFields: [ + 'browser', 'module', 'main' + ], + keepNames: true, + write: false, +}) + +if (watch) { + await result.watch() +} diff --git a/cypress/e2e/index.spec.ts b/cypress/e2e/index.spec.ts index 8b168bf12..e9d97ab06 100644 --- a/cypress/e2e/index.spec.ts +++ b/cypress/e2e/index.spec.ts @@ -1,15 +1,5 @@ /// -import type { AppOptions } from '../../src/optionsStorage' - -const cleanVisit = (url?) => { - cy.clearLocalStorage() - visit(url) -} - -const visit = (url = '/') => { - window.localStorage.cypress = 'true' - cy.visit(url) -} +import { setOptions, cleanVisit, visit } from './shared' // todo use ssl @@ -31,12 +21,6 @@ const testWorldLoad = () => { }) } -const setOptions = (options: Partial) => { - cy.window().then(win => { - Object.assign(win['options'], options) - }) -} - it('Loads & renders singleplayer', () => { cleanVisit('/?singleplayer=1') setOptions({ diff --git a/cypress/e2e/performance.spec.ts b/cypress/e2e/performance.spec.ts new file mode 100644 index 000000000..f2fc4d46e --- /dev/null +++ b/cypress/e2e/performance.spec.ts @@ -0,0 +1,25 @@ +import { cleanVisit, setOptions } from './shared' + +it('Loads & renders singleplayer', () => { + cleanVisit('/?singleplayer=1') + setOptions({ + renderDistance: 2 + }) + // wait for .initial-loader to disappear + cy.get('.initial-loader', { timeout: 20_000 }).should('not.exist') + cy.window() + .its('performance') + .invoke('mark', 'worldLoad') + + cy.document().then({ timeout: 20_000 }, doc => { + return new Cypress.Promise(resolve => { + doc.addEventListener('cypress-world-ready', resolve) + }) + }).then(() => { + const duration = cy.window() + .its('performance') + .invoke('measure', 'modalOpen') + .its('duration') + cy.log('Duration', duration) + }) +}) diff --git a/cypress/e2e/shared.ts b/cypress/e2e/shared.ts new file mode 100644 index 000000000..9292a8d55 --- /dev/null +++ b/cypress/e2e/shared.ts @@ -0,0 +1,15 @@ +import { AppOptions } from '../../src/optionsStorage' + +export const cleanVisit = (url?) => { + cy.clearLocalStorage() + visit(url) +} +export const visit = (url = '/') => { + window.localStorage.cypress = 'true' + cy.visit(url) +} +export const setOptions = (options: Partial) => { + cy.window().then(win => { + Object.assign(win['options'], options) + }) +} diff --git a/esbuild.mjs b/esbuild.mjs index 2d18ae3a0..6f7f903ee 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -11,7 +11,9 @@ import { build } from 'esbuild' //@ts-ignore try { await import('./localSettings.mjs') } catch { } -fs.writeFileSync('dist/index.html', fs.readFileSync('index.html', 'utf8').replace('', ''), 'utf8') +const entrypoint = 'index.ts' + +fs.writeFileSync('dist/index.html', fs.readFileSync('index.html', 'utf8').replace('', ``), 'utf8') const watch = process.argv.includes('--watch') || process.argv.includes('-w') const prod = process.argv.includes('--prod') @@ -30,7 +32,7 @@ const buildingVersion = new Date().toISOString().split(':')[0] /** @type {import('esbuild').BuildOptions} */ const buildOptions = { bundle: true, - entryPoints: ['src/index.ts'], + entryPoints: [`src/${entrypoint}`], target: ['es2020'], jsx: 'automatic', jsxDev: dev, @@ -76,7 +78,9 @@ const buildOptions = { loader: { // todo use external or resolve issues with duplicating '.png': 'dataurl', - '.map': 'empty' + '.map': 'empty', + '.vert': 'text', + '.frag': 'text' }, write: false, // todo would be better to enable? diff --git a/experiments/texture-render.html b/experiments/texture-render.html index be406102a..2a2716bde 100644 --- a/experiments/texture-render.html +++ b/experiments/texture-render.html @@ -10,8 +10,8 @@ diff --git a/package.json b/package.json index 881be9cc6..04cf44adb 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,10 @@ "scripts": { "start": "node scripts/build.js copyFilesDev && node scripts/prepareData.mjs && node esbuild.mjs --watch", "start-watch-script": "nodemon -w esbuild.mjs --watch", - "build": "node scripts/build.js copyFiles && node scripts/prepareData.mjs -f && node esbuild.mjs --minify --prod", + "build": "node scripts/build.js copyFiles && node scripts/prepareData.mjs -f && node esbuild.mjs --minify --prod && node buildWorkers.mjs", "check-build": "tsc && pnpm test-unit && pnpm build", "test:cypress": "cypress run", + "test:cypress:perf": "cypress run --spec cypress/e2e/perf.spec.ts --browser edge", "test-unit": "vitest", "test:e2e": "start-test http-get://localhost:8080 test:cypress", "prod-start": "node server.js", @@ -17,7 +18,12 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build && node scripts/build.js moveStorybookFiles", "start-experiments": "vite --config experiments/vite.config.ts", - "watch-worker": "node prismarine-viewer/buildWorker.mjs -w" + "watch-worker": "node prismarine-viewer/buildWorker.mjs -w", + "watch-other-workers": "node buildWorkers.mjs -w", + "run-playground": "run-p watch-worker watch-other-workers playground-server watch-playground", + "run-all": "run-p start run-playground", + "playground-server": "live-server --port=9090 prismarine-viewer/public", + "watch-playground": "node prismarine-viewer/esbuild.mjs -w" }, "keywords": [ "prismarine", @@ -56,6 +62,7 @@ "jszip": "^3.10.1", "lit": "^2.8.0", "lodash-es": "^4.17.21", + "math.gl": "^4.0.0", "minecraft-assets": "^1.12.2", "minecraft-data": "3.62.0", "net-browserify": "github:zardoy/prismarinejs-net-browserify", @@ -79,6 +86,7 @@ "stats.js": "^0.17.0", "tabbable": "^6.2.0", "title-case": "3.x", + "twgl.js": "^5.5.4", "ua-parser-js": "^1.0.37", "valtio": "^1.11.1", "workbox-build": "^7.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 483f0277f..81363a742 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: lodash-es: specifier: ^4.17.21 version: 4.17.21 + math.gl: + specifier: ^4.0.0 + version: 4.0.1 minecraft-assets: specifier: ^1.12.2 version: 1.12.2 @@ -174,6 +177,9 @@ importers: title-case: specifier: 3.x version: 3.0.3 + twgl.js: + specifier: ^5.5.4 + version: 5.5.4 ua-parser-js: specifier: ^1.0.37 version: 1.0.37 @@ -350,6 +356,9 @@ importers: lil-gui: specifier: ^0.18.2 version: 0.18.2 + live-server: + specifier: ^1.2.2 + version: 1.2.2 looks-same: specifier: ^8.2.3 version: 8.2.3 @@ -3220,6 +3229,16 @@ packages: - supports-color dev: false + /@math.gl/core@4.0.1: + resolution: {integrity: sha512-9IewNjR9V66o+gYIIq5agFoHy6ZT6DRpRGQBfsUpZz4glAqOjVt64he8GGzjpmqfT+kKT4qwQ7nQl/hZLF15qA==} + dependencies: + '@math.gl/types': 4.0.1 + dev: false + + /@math.gl/types@4.0.1: + resolution: {integrity: sha512-E9qBKAjVBiZD8Is7TbygiLGtYBP3GSLus6RUJSuzFQegdYXeVagvrs4UkBJxhrRAxw4crfH0Tq7IhTMKuuJNQw==} + dev: false + /@mdx-js/react@2.3.0(react@18.2.0): resolution: {integrity: sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==} peerDependencies: @@ -5718,7 +5737,7 @@ packages: prismarine-nbt: 2.5.0 prismarine-provider-anvil: github.com/zardoy/prismarine-provider-anvil/0ddcd9d48574113308e1fbebef60816aced0846f(minecraft-data@3.62.0) prismarine-windows: 2.9.0 - prismarine-world: github.com/zardoy/prismarine-world/c358222204d21fe7d45379fbfcefb047f926c786 + prismarine-world: github.com/zardoy/prismarine-world/6ae6f009d38460de284f8c226c665f04cbad9465 random-seed: 0.3.0 range: 0.0.3 readline: 1.3.0 @@ -5933,6 +5952,15 @@ packages: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} dev: false + /anymatch@2.0.0: + resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} + dependencies: + micromatch: 3.1.10 + normalize-path: 2.1.1 + transitivePeerDependencies: + - supports-color + dev: false + /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -5941,6 +5969,18 @@ packages: picomatch: 2.3.1 dev: true + /apache-crypt@1.2.6: + resolution: {integrity: sha512-072WetlM4blL8PREJVeY+WHiUh1R5VNt2HfceGS8aKqttPHcmqE5pkKuXPz/ULmJOFkc8Hw3kfKl6vy7Qka6DA==} + engines: {node: '>=8'} + dependencies: + unix-crypt-td-js: 1.1.4 + dev: false + + /apache-md5@1.1.8: + resolution: {integrity: sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA==} + engines: {node: '>=8'} + dev: false + /app-root-dir@1.0.2: resolution: {integrity: sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==} dev: true @@ -5986,6 +6026,21 @@ packages: tslib: 2.6.2 dev: true + /arr-diff@4.0.0: + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} + dev: false + + /arr-flatten@1.1.0: + resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} + engines: {node: '>=0.10.0'} + dev: false + + /arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + dev: false + /array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} dependencies: @@ -6019,6 +6074,11 @@ packages: engines: {node: '>=8'} dev: true + /array-unique@0.3.2: + resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} + engines: {node: '>=0.10.0'} + dev: false + /array.prototype.flat@1.3.2: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} @@ -6121,6 +6181,11 @@ packages: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true + /assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + dev: false + /ast-types@0.14.2: resolution: {integrity: sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==} engines: {node: '>=4'} @@ -6146,6 +6211,10 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} + /async-each@1.0.6: + resolution: {integrity: sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==} + dev: false + /async-limiter@1.0.1: resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} @@ -6170,6 +6239,12 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} + /atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + dev: false + /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} @@ -6279,18 +6354,38 @@ packages: engines: {node: ^4.5.0 || >= 5.9} dev: false + /base@0.11.2: + resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} + engines: {node: '>=0.10.0'} + dependencies: + cache-base: 1.0.1 + class-utils: 0.3.6 + component-emitter: 1.3.0 + define-property: 1.0.0 + isobject: 3.0.1 + mixin-deep: 1.3.2 + pascalcase: 0.1.1 + dev: false + /basic-auth@2.0.1: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} dependencies: safe-buffer: 5.1.2 - dev: true + + /batch@0.6.1: + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + dev: false /bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} dependencies: tweetnacl: 0.14.5 + /bcryptjs@2.4.3: + resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + dev: false + /better-opn@3.0.2: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} @@ -6303,6 +6398,11 @@ packages: engines: {node: '>=0.6'} dev: true + /binary-extensions@1.13.1: + resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==} + engines: {node: '>=0.10.0'} + dev: false + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -6412,6 +6512,24 @@ packages: dependencies: balanced-match: 1.0.2 + /braces@2.3.2: + resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} + engines: {node: '>=0.10.0'} + dependencies: + arr-flatten: 1.1.0 + array-unique: 0.3.2 + extend-shallow: 2.0.1 + fill-range: 4.0.0 + isobject: 3.0.1 + repeat-element: 1.1.4 + snapdragon: 0.8.2 + snapdragon-node: 2.1.1 + split-string: 3.1.0 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: false + /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} @@ -6602,6 +6720,21 @@ packages: dev: false optional: true + /cache-base@1.0.1: + resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} + engines: {node: '>=0.10.0'} + dependencies: + collection-visit: 1.0.0 + component-emitter: 1.3.0 + get-value: 2.0.6 + has-value: 1.0.0 + isobject: 3.0.1 + set-value: 2.0.1 + to-object-path: 0.3.0 + union-value: 1.0.1 + unset-value: 1.0.0 + dev: false + /cachedir@2.4.0: resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} engines: {node: '>=6'} @@ -6765,6 +6898,27 @@ packages: resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} engines: {node: '>= 0.8.0'} + /chokidar@2.1.8: + resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==} + deprecated: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies + dependencies: + anymatch: 2.0.0 + async-each: 1.0.6 + braces: 2.3.2 + glob-parent: 3.1.0 + inherits: 2.0.4 + is-binary-path: 1.0.1 + is-glob: 4.0.3 + normalize-path: 3.0.0 + path-is-absolute: 1.0.1 + readdirp: 2.2.1 + upath: 1.2.0 + optionalDependencies: + fsevents: 1.2.13 + transitivePeerDependencies: + - supports-color + dev: false + /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -6798,6 +6952,16 @@ packages: safe-buffer: 5.2.1 dev: true + /class-utils@0.3.6: + resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} + engines: {node: '>=0.10.0'} + dependencies: + arr-union: 3.1.0 + define-property: 0.2.5 + isobject: 3.0.1 + static-extend: 0.1.2 + dev: false + /classnames@2.3.2: resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} dev: false @@ -6880,6 +7044,14 @@ packages: engines: {node: '>=6'} dev: false + /collection-visit@1.0.0: + resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} + engines: {node: '>=0.10.0'} + dependencies: + map-visit: 1.0.0 + object-visit: 1.0.1 + dev: false + /color-convert@0.5.3: resolution: {integrity: sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==} dev: false @@ -7012,6 +7184,18 @@ packages: resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} dev: true + /connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + dev: false + /console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} dev: false @@ -7069,6 +7253,11 @@ packages: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} + /copy-descriptor@0.1.1: + resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} + engines: {node: '>=0.10.0'} + dev: false + /copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} dependencies: @@ -7392,6 +7581,11 @@ packages: character-entities: 2.0.2 dev: false + /decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + dev: false + /decompress-response@4.2.1: resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==} engines: {node: '>=8'} @@ -7471,6 +7665,28 @@ packages: has-property-descriptors: 1.0.0 object-keys: 1.1.1 + /define-property@0.2.5: + resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} + engines: {node: '>=0.10.0'} + dependencies: + is-descriptor: 0.1.7 + dev: false + + /define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} + dependencies: + is-descriptor: 1.0.3 + dev: false + + /define-property@2.0.2: + resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-descriptor: 1.0.3 + isobject: 3.0.1 + dev: false + /defu@6.1.2: resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==} dev: true @@ -7497,6 +7713,11 @@ packages: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false + /depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + dev: false + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -7685,6 +7906,10 @@ packages: resolution: {integrity: sha512-+3NaRjWktb5r61ZFoDejlykPEFKT5N/LkbXsaddlw6xNSXBanUYpFc2AXXpbJDilPHazcSreU/DpQIaxfX0NfQ==} dev: false + /duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + dev: false + /duplexify@3.7.1: resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} dependencies: @@ -8616,6 +8841,18 @@ packages: promise: 5.0.0 dev: false + /event-stream@3.3.4: + resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} + dependencies: + duplexer: 0.1.2 + from: 0.1.7 + map-stream: 0.1.0 + pause-stream: 0.0.11 + split: 0.3.3 + stream-combiner: 0.0.4 + through: 2.3.8 + dev: false + /event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -8681,6 +8918,21 @@ packages: engines: {node: '>=6'} dev: false + /expand-brackets@2.1.4: + resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} + engines: {node: '>=0.10.0'} + dependencies: + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + posix-character-classes: 0.1.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: false + /expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -8744,9 +8996,40 @@ packages: transitivePeerDependencies: - supports-color + /extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + dependencies: + is-extendable: 0.1.1 + dev: false + + /extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + dev: false + /extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + /extglob@2.0.4: + resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} + engines: {node: '>=0.10.0'} + dependencies: + array-unique: 0.3.2 + define-property: 1.0.0 + expand-brackets: 2.1.4 + extend-shallow: 2.0.1 + fragment-cache: 0.2.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: false + /extract-zip@1.7.0: resolution: {integrity: sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==} hasBin: true @@ -8815,6 +9098,13 @@ packages: reusify: 1.0.4 dev: true + /faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + dependencies: + websocket-driver: 0.7.4 + dev: false + /fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} dependencies: @@ -8873,6 +9163,16 @@ packages: resolution: {integrity: sha512-6RS9gDchbn+qWmtV2uSjo5vmKizgfCQeb5jKmqx8HyzA3MoLqqyQxN+QcjkGBJt7FjJ9qFce67Auyya5rRRbpw==} engines: {node: '>= 10.4.0'} + /fill-range@4.0.0: + resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 2.0.1 + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range: 2.1.1 + dev: false + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -8880,6 +9180,21 @@ packages: to-regex-range: 5.0.1 dev: true + /finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /finalhandler@1.2.0: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} engines: {node: '>= 0.8'} @@ -8989,6 +9304,11 @@ packages: dependencies: is-callable: 1.2.7 + /for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + dev: false + /foreground-child@2.0.0: resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} engines: {node: '>=8.0.0'} @@ -9029,10 +9349,21 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + /fragment-cache@0.2.1: + resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} + engines: {node: '>=0.10.0'} + dependencies: + map-cache: 0.2.2 + dev: false + /fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + /from@0.1.7: + resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + dev: false + /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -9080,6 +9411,18 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + /fsevents@1.2.13: + resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} + engines: {node: '>= 4.0'} + os: [darwin] + deprecated: The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2 + requiresBuild: true + dependencies: + bindings: 1.5.0 + nan: 2.18.0 + dev: false + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -9223,6 +9566,11 @@ packages: resolve-pkg-maps: 1.0.0 dev: false + /get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + dev: false + /getos@3.2.1: resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} dependencies: @@ -9275,6 +9623,13 @@ packages: dev: false optional: true + /glob-parent@3.1.0: + resolution: {integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==} + dependencies: + is-glob: 3.1.0 + path-dirname: 1.0.2 + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -9486,6 +9841,37 @@ packages: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} dev: false + /has-value@0.3.1: + resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} + engines: {node: '>=0.10.0'} + dependencies: + get-value: 2.0.6 + has-values: 0.1.4 + isobject: 2.1.0 + dev: false + + /has-value@1.0.0: + resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} + engines: {node: '>=0.10.0'} + dependencies: + get-value: 2.0.6 + has-values: 1.0.0 + isobject: 3.0.1 + dev: false + + /has-values@0.1.4: + resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} + engines: {node: '>=0.10.0'} + dev: false + + /has-values@1.0.0: + resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-number: 3.0.0 + kind-of: 4.0.0 + dev: false + /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} @@ -9569,6 +9955,16 @@ packages: engines: {node: '>=8'} dev: true + /http-auth@3.1.3: + resolution: {integrity: sha512-Jbx0+ejo2IOx+cRUYAGS1z6RGc6JfYUNkysZM4u4Sfk1uLlGv814F7/PIjQQAuThLdAWxb74JMGd5J8zex1VQg==} + engines: {node: '>=4.6.1'} + dependencies: + apache-crypt: 1.2.6 + apache-md5: 1.1.8 + bcryptjs: 2.4.3 + uuid: 3.4.0 + dev: false + /http-browserify@1.7.0: resolution: {integrity: sha512-Irf/LJXmE3cBzU1eaR4+NEX6bmVLqt1wkmDiA7kBwH7zmb0D8kBAXsDmQ88hhj/qv9iEZKlyGx/hrMcFi8sOHw==} dependencies: @@ -9579,6 +9975,16 @@ packages: /http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + /http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 + dev: false + /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -9589,6 +9995,10 @@ packages: statuses: 2.0.1 toidentifier: 1.0.1 + /http-parser-js@0.5.8: + resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} + dev: false + /http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} @@ -9779,6 +10189,10 @@ packages: once: 1.4.0 wrappy: 1.0.2 + /inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + dev: false + /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -9833,6 +10247,13 @@ packages: engines: {node: '>=8'} dev: true + /is-accessor-descriptor@1.0.1: + resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} + engines: {node: '>= 0.10'} + dependencies: + hasown: 2.0.1 + dev: false + /is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} @@ -9874,6 +10295,13 @@ packages: dependencies: has-bigints: 1.0.2 + /is-binary-path@1.0.1: + resolution: {integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==} + engines: {node: '>=0.10.0'} + dependencies: + binary-extensions: 1.13.1 + dev: false + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -9888,6 +10316,10 @@ packages: call-bind: 1.0.2 has-tostringtag: 1.0.0 + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false + /is-builtin-module@3.2.1: resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} @@ -9916,6 +10348,13 @@ packages: hasown: 2.0.1 dev: true + /is-data-descriptor@1.0.1: + resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} + engines: {node: '>= 0.4'} + dependencies: + hasown: 2.0.1 + dev: false + /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} @@ -9926,16 +10365,43 @@ packages: resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} dev: true + /is-descriptor@0.1.7: + resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==} + engines: {node: '>= 0.4'} + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + dev: false + + /is-descriptor@1.0.3: + resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==} + engines: {node: '>= 0.4'} + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + dev: false + /is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} hasBin: true dev: true + /is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + dev: false + + /is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + dependencies: + is-plain-object: 2.0.4 + dev: false + /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - dev: true /is-finalizationregistry@1.0.2: resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} @@ -9957,12 +10423,18 @@ packages: dependencies: has-tostringtag: 1.0.0 + /is-glob@3.1.0: + resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: false + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 - dev: true /is-gzip@1.0.0: resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==} @@ -10012,6 +10484,13 @@ packages: dependencies: has-tostringtag: 1.0.0 + /is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: false + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -10046,7 +10525,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: isobject: 3.0.1 - dev: true /is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} @@ -10126,6 +10604,16 @@ packages: get-intrinsic: 1.2.4 dev: true + /is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + dev: false + + /is-wsl@1.1.0: + resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==} + engines: {node: '>=4'} + dev: false + /is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -10152,10 +10640,16 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /isobject@2.1.0: + resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} + engines: {node: '>=0.10.0'} + dependencies: + isarray: 1.0.0 + dev: false + /isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} - dev: true /isstream@0.1.2: resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} @@ -10500,10 +10994,23 @@ packages: json-buffer: 3.0.1 dev: true + /kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-buffer: 1.1.6 + dev: false + + /kind-of@4.0.0: + resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} + engines: {node: '>=0.10.0'} + dependencies: + is-buffer: 1.1.6 + dev: false + /kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - dev: true /kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} @@ -10596,6 +11103,28 @@ packages: lit-element: 3.3.3 lit-html: 2.8.0 + /live-server@1.2.2: + resolution: {integrity: sha512-t28HXLjITRGoMSrCOv4eZ88viHaBVIjKjdI5PO92Vxlu+twbk6aE0t7dVIaz6ZWkjPilYFV6OSdMYl9ybN2B4w==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + chokidar: 2.1.8 + colors: 1.4.0 + connect: 3.7.0 + cors: 2.8.5 + event-stream: 3.3.4 + faye-websocket: 0.11.4 + http-auth: 3.1.3 + morgan: 1.10.0 + object-assign: 4.1.1 + opn: 6.0.0 + proxy-middleware: 0.15.0 + send: 0.18.0 + serve-index: 1.9.1 + transitivePeerDependencies: + - supports-color + dev: false + /load-bmfont@1.4.1: resolution: {integrity: sha512-8UyQoYmdRDy81Brz6aLAUhfZLwr5zV0L3taTQ4hju7m6biuwiWiJXjPhBJxbUQJA8PrkvJ/7Enqmwk2sM14soA==} dependencies: @@ -10848,6 +11377,11 @@ packages: tmpl: 1.0.5 dev: true + /map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + dev: false + /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -10862,6 +11396,17 @@ packages: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} dev: true + /map-stream@0.1.0: + resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} + dev: false + + /map-visit@1.0.0: + resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} + engines: {node: '>=0.10.0'} + dependencies: + object-visit: 1.0.1 + dev: false + /markdown-it@14.0.0: resolution: {integrity: sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==} hasBin: true @@ -10883,6 +11428,12 @@ packages: react: 18.2.0 dev: true + /math.gl@4.0.1: + resolution: {integrity: sha512-Yvw1HfmsDePxwhCBvGT8teyPN0mwxcxUaWLoDaRuZYxoUYa9HRg+6ywBS+yXopC0wIS9MFi+BCVKP8hdJpaJjw==} + dependencies: + '@math.gl/core': 4.0.1 + dev: false + /md5-file@4.0.0: resolution: {integrity: sha512-UC0qFwyAjn4YdPpKaDNw6gNxRf7Mcx7jC1UGCY4boCzgvU2Aoc1mOGzTtrjjLKhM5ivsnhoKpQVxKPp+1j1qwg==} engines: {node: '>=6.0'} @@ -11190,6 +11741,27 @@ packages: - supports-color dev: false + /micromatch@3.1.10: + resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} + engines: {node: '>=0.10.0'} + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + braces: 2.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + extglob: 2.0.4 + fragment-cache: 0.2.1 + kind-of: 6.0.3 + nanomatch: 1.2.13 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: false + /micromatch@4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} @@ -11411,6 +11983,14 @@ packages: minipass: 3.3.6 yallist: 4.0.0 + /mixin-deep@1.3.2: + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} + dependencies: + for-in: 1.0.2 + is-extendable: 1.0.1 + dev: false + /mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -11457,6 +12037,19 @@ packages: /moo@0.5.2: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} + /morgan@1.10.0: + resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} + engines: {node: '>= 0.8.0'} + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.0.2 + transitivePeerDependencies: + - supports-color + dev: false + /mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -11512,6 +12105,25 @@ packages: hasBin: true dev: true + /nanomatch@1.2.13: + resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} + engines: {node: '>=0.10.0'} + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + fragment-cache: 0.2.1 + is-windows: 1.0.2 + kind-of: 6.0.3 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: false + /napi-build-utils@1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} requiresBuild: true @@ -11721,10 +12333,16 @@ packages: validate-npm-package-license: 3.0.4 dev: true + /normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + dependencies: + remove-trailing-separator: 1.1.0 + dev: false + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - dev: true /npm-run-all@4.1.5: resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} @@ -11779,6 +12397,15 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + /object-copy@0.1.0: + resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} + engines: {node: '>=0.10.0'} + dependencies: + copy-descriptor: 0.1.1 + define-property: 0.2.5 + kind-of: 3.2.2 + dev: false + /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} @@ -11796,6 +12423,13 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + /object-visit@1.0.1: + resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: false + /object.assign@4.1.4: resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} engines: {node: '>= 0.4'} @@ -11840,6 +12474,13 @@ packages: es-abstract: 1.22.4 dev: true + /object.pick@1.3.0: + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: false + /object.values@1.1.7: resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} engines: {node: '>= 0.4'} @@ -11853,6 +12494,13 @@ packages: resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} dev: false + /on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + /on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -11888,6 +12536,14 @@ packages: hasBin: true dev: true + /opn@6.0.0: + resolution: {integrity: sha512-I9PKfIZC+e4RXZ/qr1RhgyCnGgYX0UEIlXgWnCOVACIvFgaC9rz6Won7xbdhoHrd8IIhV7YEpHjreNUNkqCGkQ==} + engines: {node: '>=8'} + deprecated: The package has been renamed to `open` + dependencies: + is-wsl: 1.1.0 + dev: false + /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -12070,6 +12726,11 @@ packages: tslib: 2.6.2 dev: false + /pascalcase@0.1.1: + resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} + engines: {node: '>=0.10.0'} + dev: false + /path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} dev: true @@ -12081,6 +12742,10 @@ packages: tslib: 2.6.2 dev: false + /path-dirname@1.0.2: + resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==} + dev: false + /path-exists-cli@2.0.0: resolution: {integrity: sha512-qGr0A87KYCznmvabblxyxnzA/MtPZ28wH+4SCMP4tjTFAbzqwvs5xpUZExAYzq5OgHe5vIswzdH5iosCb8YF/Q==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -12151,6 +12816,12 @@ packages: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} dev: true + /pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + dependencies: + through: 2.3.8 + dev: false + /pbkdf2@3.1.2: resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} engines: {node: '>=0.12'} @@ -12296,6 +12967,11 @@ packages: - supports-color dev: true + /posix-character-classes@0.1.1: + resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} + engines: {node: '>=0.10.0'} + dev: false + /postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -12483,7 +13159,7 @@ packages: minecraft-data: 3.62.0 prismarine-block: github.com/zardoy/prismarine-block/ada4ec3fdfbbc1cc20ab01d0e23f0718a77cc1a0 prismarine-nbt: 2.2.1 - prismarine-world: github.com/zardoy/prismarine-world/c358222204d21fe7d45379fbfcefb047f926c786 + prismarine-world: github.com/zardoy/prismarine-world/6ae6f009d38460de284f8c226c665f04cbad9465 vec3: 0.1.8 dev: false @@ -12695,6 +13371,11 @@ packages: /proxy-from-env@1.0.0: resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} + /proxy-middleware@0.15.0: + resolution: {integrity: sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==} + engines: {node: '>=0.8.0'} + dev: false + /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} @@ -13203,6 +13884,17 @@ packages: process: 0.11.10 string_decoder: 1.3.0 + /readdirp@2.2.1: + resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==} + engines: {node: '>=0.10'} + dependencies: + graceful-fs: 4.2.11 + micromatch: 3.1.10 + readable-stream: 2.3.8 + transitivePeerDependencies: + - supports-color + dev: false + /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -13277,6 +13969,14 @@ packages: dependencies: '@babel/runtime': 7.22.11 + /regex-not@1.0.2: + resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 3.0.2 + safe-regex: 1.1.0 + dev: false + /regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true @@ -13377,6 +14077,20 @@ packages: - supports-color dev: false + /remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + dev: false + + /repeat-element@1.1.4: + resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} + engines: {node: '>=0.10.0'} + dev: false + + /repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + dev: false + /request-progress@3.0.0: resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} dependencies: @@ -13411,6 +14125,11 @@ packages: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: false + /resolve-url@0.2.1: + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + deprecated: https://github.com/lydell/resolve-url#deprecated + dev: false + /resolve@1.22.4: resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} hasBin: true @@ -13578,6 +14297,12 @@ packages: is-regex: 1.1.4 dev: true + /safe-regex@1.1.0: + resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + dependencies: + ret: 0.1.15 + dev: false + /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -13670,6 +14395,21 @@ packages: randombytes: 2.1.0 dev: false + /serve-index@1.9.1: + resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} + engines: {node: '>= 0.8.0'} + dependencies: + accepts: 1.3.8 + batch: 0.6.1 + debug: 2.6.9 + escape-html: 1.0.3 + http-errors: 1.6.3 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + dev: false + /serve-static@1.15.0: resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} engines: {node: '>= 0.8.0'} @@ -13709,9 +14449,23 @@ packages: engines: {node: '>=6.9'} dev: false + /set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + dev: false + /setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + /setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + dev: false + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -13880,6 +14634,38 @@ packages: tslib: 2.6.2 dev: false + /snapdragon-node@2.1.1: + resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} + engines: {node: '>=0.10.0'} + dependencies: + define-property: 1.0.0 + isobject: 3.0.1 + snapdragon-util: 3.0.1 + dev: false + + /snapdragon-util@3.0.1: + resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: false + + /snapdragon@0.8.2: + resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} + engines: {node: '>=0.10.0'} + dependencies: + base: 0.11.2 + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + map-cache: 0.2.2 + source-map: 0.5.7 + source-map-resolve: 0.5.3 + use: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: false + /socket.io-adapter@1.1.2: resolution: {integrity: sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==} dev: false @@ -14017,12 +14803,28 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + /source-map-resolve@0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 + dev: false + /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + /source-map-url@0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + dev: false + /source-map@0.5.6: resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} engines: {node: '>=0.10.0'} @@ -14075,6 +14877,19 @@ packages: resolution: {integrity: sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==} dev: true + /split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 3.0.2 + dev: false + + /split@0.3.3: + resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} + dependencies: + through: 2.3.8 + dev: false + /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true @@ -14132,6 +14947,14 @@ packages: stacktrace-gps: 3.1.2 dev: false + /static-extend@0.1.2: + resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} + engines: {node: '>=0.10.0'} + dependencies: + define-property: 0.2.5 + object-copy: 0.1.0 + dev: false + /stats-gl@1.0.5: resolution: {integrity: sha512-XimMxvwnf1Qf5KwebhcoA34kcX+fWEkIl0QjNkCbu4IpoyDMMsOajExn7FIq5w569k45+LhmsuRlGSrsvmGdNw==} dev: false @@ -14140,6 +14963,11 @@ packages: resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} dev: false + /statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + dev: false + /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -14171,6 +14999,12 @@ packages: readable-stream: 3.6.2 dev: true + /stream-combiner@0.0.4: + resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + dependencies: + duplexer: 0.1.2 + dev: false + /stream-shift@1.0.1: resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==} dev: true @@ -14571,6 +15405,21 @@ packages: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} + /to-object-path@0.3.0: + resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: false + + /to-regex-range@2.1.1: + resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} + engines: {node: '>=0.10.0'} + dependencies: + is-number: 3.0.0 + repeat-string: 1.6.1 + dev: false + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -14578,6 +15427,16 @@ packages: is-number: 7.0.0 dev: true + /to-regex@3.0.2: + resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} + engines: {node: '>=0.10.0'} + dependencies: + define-property: 2.0.2 + extend-shallow: 3.0.2 + regex-not: 1.0.2 + safe-regex: 1.1.0 + dev: false + /tocbot@4.21.2: resolution: {integrity: sha512-R5Muhi/TUu4i4snWVrMgNoXyJm2f8sJfdgIkQvqb+cuIXQEIMAiWGWgCgYXHqX4+XiS/Bnm7IYZ9Zy6NVe6lhw==} dev: true @@ -14676,6 +15535,10 @@ packages: /tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + /twgl.js@5.5.4: + resolution: {integrity: sha512-6kFOmijOpmblTN9CCwOTCxK4lPg7rCyQjLuub6EMOlEp89Ex6yUcsMjsmH7andNPL2NE3XmHdqHeP5gVKKPhxw==} + dev: false + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -14867,6 +15730,16 @@ packages: vfile: 6.0.1 dev: false + /union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + dev: false + /union@0.5.0: resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==} engines: {node: '>= 0.8.0'} @@ -14957,6 +15830,10 @@ packages: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} + /unix-crypt-td-js@1.1.4: + resolution: {integrity: sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==} + dev: false + /unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -14970,6 +15847,14 @@ packages: webpack-virtual-modules: 0.5.0 dev: true + /unset-value@1.0.0: + resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} + engines: {node: '>=0.10.0'} + dependencies: + has-value: 0.3.1 + isobject: 3.0.1 + dev: false + /untildify@4.0.0: resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} engines: {node: '>=8'} @@ -15006,6 +15891,11 @@ packages: dependencies: punycode: 2.3.0 + /urix@0.1.0: + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + deprecated: Please see https://github.com/lydell/urix#deprecated + dev: false + /url-join@4.0.1: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} dev: true @@ -15090,6 +15980,11 @@ packages: use-deep-compare: 1.1.0(react@18.2.0) dev: true + /use@3.1.1: + resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} + engines: {node: '>=0.10.0'} + dev: false + /utf8-byte-length@1.0.4: resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==} dev: false @@ -15121,6 +16016,12 @@ packages: dependencies: macaddress: 0.5.3 + /uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: false + /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -15403,6 +16304,20 @@ packages: sdp: 3.2.0 dev: false + /websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + dependencies: + http-parser-js: 0.5.8 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + dev: false + + /websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + dev: false + /whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} @@ -15929,7 +16844,7 @@ packages: prismarine-recipe: 1.3.1(prismarine-registry@1.7.0) prismarine-registry: 1.7.0 prismarine-windows: 2.9.0 - prismarine-world: github.com/zardoy/prismarine-world/c358222204d21fe7d45379fbfcefb047f926c786 + prismarine-world: github.com/zardoy/prismarine-world/6ae6f009d38460de284f8c226c665f04cbad9465 protodef: 1.15.0 typed-emitter: 1.4.0 vec3: 0.1.8 @@ -16037,8 +16952,8 @@ packages: - minecraft-data dev: false - github.com/zardoy/prismarine-world/c358222204d21fe7d45379fbfcefb047f926c786: - resolution: {tarball: https://codeload.github.com/zardoy/prismarine-world/tar.gz/c358222204d21fe7d45379fbfcefb047f926c786} + github.com/zardoy/prismarine-world/6ae6f009d38460de284f8c226c665f04cbad9465: + resolution: {tarball: https://codeload.github.com/zardoy/prismarine-world/tar.gz/6ae6f009d38460de284f8c226c665f04cbad9465} name: prismarine-world version: 3.6.2 engines: {node: '>=8.0.0'} diff --git a/prismarine-viewer/buildWorker.mjs b/prismarine-viewer/buildWorker.mjs index a8a5c1381..9be06cf23 100644 --- a/prismarine-viewer/buildWorker.mjs +++ b/prismarine-viewer/buildWorker.mjs @@ -21,7 +21,7 @@ const buildOptions = { }, platform: 'browser', entryPoints: [path.join(__dirname, './viewer/lib/worker.js')], - minify: true, + // minify: true, logLevel: 'info', drop: !watch ? [ 'debugger' @@ -101,7 +101,7 @@ const buildOptions = { resolveDir: process.cwd(), } }) - build.onEnd(({metafile, outputFiles}) => { + build.onEnd(({ metafile, outputFiles }) => { if (!metafile) return fs.writeFileSync(path.join(__dirname, './public/metafile.json'), JSON.stringify(metafile)) for (const outDir of ['../dist/', './public/']) { diff --git a/prismarine-viewer/esbuild.mjs b/prismarine-viewer/esbuild.mjs index 91b787db9..de1a3b25f 100644 --- a/prismarine-viewer/esbuild.mjs +++ b/prismarine-viewer/esbuild.mjs @@ -47,11 +47,14 @@ const buildOptions = { http: 'http-browserify', stream: 'stream-browserify', net: 'net-browserify', + 'stats.js': 'stats.js/src/Stats.js', }, inject: [], metafile: true, loader: { '.png': 'dataurl', + '.vert': 'text', + '.frag': 'text' }, plugins: [ { diff --git a/prismarine-viewer/examples/TextureAnimation.ts b/prismarine-viewer/examples/TextureAnimation.ts new file mode 100644 index 000000000..dfa004624 --- /dev/null +++ b/prismarine-viewer/examples/TextureAnimation.ts @@ -0,0 +1,69 @@ +export type AnimationControlSwitches = { + tick: number + interpolationTick: number // next one +} + +type Data = { + interpolate: boolean; + frametime: number; + frames: ({ + index: number; + time: number; + } | number)[] | undefined; +}; + +export class TextureAnimation { + data: Data; + frameImages: number; + frameDelta: number; + frameTime: number; + framesToSwitch: number; + frameIndex: number; + + constructor(public animationControl: AnimationControlSwitches, data: Data, public framesImages: number) { + this.data = { + interpolate: false, + frametime: 1, + ...data + }; + this.frameImages = 1; + this.frameDelta = 0; + this.frameTime = this.data.frametime * 50; + this.frameIndex = 0; + + this.framesToSwitch = this.frameImages; + if (this.data.frames) { + this.framesToSwitch = this.data.frames.length; + } + } + + step (deltaMs: number) { + this.frameDelta += deltaMs; + + if (this.frameDelta > this.frameTime) { + this.frameDelta -= this.frameTime; + this.frameDelta %= this.frameTime; + + this.frameIndex++; + this.frameIndex %= this.framesToSwitch; + + const frames = this.data.frames.map(frame => typeof frame === 'number' ? { index: frame, time: this.data.frametime } : frame); + if (frames) { + let frame = frames[this.frameIndex] + let nextFrame = frames[(this.frameIndex + 1) % this.framesToSwitch]; + + this.animationControl.tick = frame.index; + this.animationControl.interpolationTick = nextFrame.index; + this.frameTime = frame.time * 50; + } else { + this.animationControl.tick = this.frameIndex; + this.animationControl.interpolationTick = (this.frameIndex + 1) % this.framesToSwitch; + } + } + + if (this.data.interpolate) { + this.animationControl.interpolationTick = this.frameDelta / this.frameTime; + } + } + +} diff --git a/prismarine-viewer/examples/TouchControls2.tsx b/prismarine-viewer/examples/TouchControls2.tsx new file mode 100644 index 000000000..5d1b4459f --- /dev/null +++ b/prismarine-viewer/examples/TouchControls2.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react' +import { LeftTouchArea, RightTouchArea, useInterfaceState } from '@dimaka/interface' +import { css } from '@emotion/css' +import { Viewer } from '../viewer/lib/viewer' +import { renderToDom } from '@zardoy/react-util' +import { Vec3 } from 'vec3' +import * as THREE from 'three' + +declare const viewer: Viewer +const Controls = () => { + // todo setting + const usingTouch = navigator.maxTouchPoints > 0 + + useEffect(() => { + let vec3 = new Vec3(0, 0, 0) + + setInterval(() => { + viewer.camera.position.add(new THREE.Vector3(vec3.x, vec3.y, vec3.z)) + }, 1000 / 30) + + useInterfaceState.setState({ + isFlying: false, + uiCustomization: { + touchButtonSize: 40, + }, + updateCoord ([coord, state]) { + vec3 = new Vec3(0, 0, 0) + vec3[coord] = state + } + }) + }, []) + + if (!usingTouch) return null + return ( +
div { + pointer-events: auto; + } + `} + > + +
+ +
+ ) +} + +export const renderPlayground = () => { + renderToDom(, { + // selector: 'body', + }) +} diff --git a/prismarine-viewer/examples/_FragmentShader.frag b/prismarine-viewer/examples/_FragmentShader.frag new file mode 100644 index 000000000..ca0c9f4dc --- /dev/null +++ b/prismarine-viewer/examples/_FragmentShader.frag @@ -0,0 +1,31 @@ +#version 300 es +precision highp float; + +out vec4 FragColor; + +in vec2 TexCoord; +flat in float TextureIndex; +flat in vec3 BiomeColor; + +uniform sampler2D texture1; + +void main() +{ + ivec2 texSize = textureSize(texture1, 0); + int TilesPerRow = texSize.x / 16; + + ivec2 coord = ivec2(16,16) * ivec2(int(TextureIndex)%TilesPerRow,int(TextureIndex)/TilesPerRow); + coord = coord + ivec2(TexCoord * 16.0f); + + vec4 t = texelFetch(texture1, coord, 0); + if (abs(t.x-t.y) <=0.010 || abs(t.x-t.z)<=0.010 ||abs(t.y-t.z) <=0.010) + { + FragColor = vec4(BiomeColor,1.0f)*t; + } + else + { + FragColor = t; + //FragColor = mix(t, vec4(BiomeColor, 1.0f), 0.5f); + } + +} diff --git a/prismarine-viewer/examples/_VertexShader.vert b/prismarine-viewer/examples/_VertexShader.vert new file mode 100644 index 000000000..0acdacf94 --- /dev/null +++ b/prismarine-viewer/examples/_VertexShader.vert @@ -0,0 +1,87 @@ +#version 300 es +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec2 aTexCoord; +layout (location = 2) in float CubeSide; + +layout (location = 3) in vec3 aOffset; +layout (location = 4) in float aTextureIndex; +layout (location = 6) in vec3 aBiomeColor; + +//#Define + +out vec2 TexCoord; +flat out float TextureIndex; +flat out vec3 BiomeColor; + +uniform mat4 view; +uniform mat4 projection; +uniform int tick; + +mat4 rotationX( in float angle ) { + return mat4( 1.0, 0, 0, 0, + 0, cos(angle), -sin(angle), 0, + 0, sin(angle), cos(angle), 0, + 0, 0, 0, 1); +} + +mat4 rotationY( in float angle ) { + return mat4( cos(angle), 0, sin(angle), 0, + 0, 1.0, 0, 0, + -sin(angle), 0, cos(angle), 0, + 0, 0, 0, 1); +} + +void main() +{ + //vec3 TransitionedPos = aPos; + vec3 TransitionedPos ;//= (vec4(aPos,0.0f) *rotationX(radians(180.0f))).xyz; + + TexCoord = vec2(aTexCoord.x, (1.0 - aTexCoord.y)); // Flipping image for opengl coordinates + TextureIndex = aTextureIndex; //Passing texture index to fragment shader + switch (int(CubeSide)) { + case 0: + TexCoord = vec2((1.0f-aTexCoord.x), (1.0 - aTexCoord.y)); + //TextureIndex = aTextureIndex.x; + //TransitionedPos = (vec4(aPos,0.0f) *rotationY(radians(90.0f))).xyz; + + TransitionedPos = (vec4(aPos,0.0f) *rotationX(radians(-90.0f))).xyz; + break; + case 1: + //TextureIndex = aTextureIndex.y; + TransitionedPos = (vec4(aPos,0.0f) *rotationX(radians(90.0f))).xyz; + break; + case 2: + //TextureIndex = aTextureIndex.z; + //TexCoord = vec2((1.0f-aTexCoord.y), (1.0f - aTexCoord.x)); + TransitionedPos = vec4(aPos,0.0f).xyz; + break; + case 3: + //TextureIndex = aTextureIndex.z; + //TexCoord = vec2(aTexCoord.y, (1.0f - aTexCoord.x)); + TexCoord = vec2(aTexCoord); + //TransitionedPos = (vec4(aPos,0.0f) *rotationX(radians(-90.0f))).xyz; + //TransitionedPos = (vec4(aPos,0.0f) *rotationY(radians(90.0f))).xyz; + TransitionedPos = (vec4(aPos,0.0f) *rotationX(radians(180.0f))).xyz; + break; + case 4: + //TextureIndex = aTextureIndexPlus.x; + //TransitionedPos = vec4(aPos,0.0f).xyz; + //TransitionedPos = (vec4(aPos,0.0f) *rotationY(radians(-90.0f))).xyz; + TransitionedPos = (vec4(aPos,0.0f) *rotationY(radians(90.0f))).xyz; + break; + case 5: + //TextureIndex = aTextureIndexPlus.y; + //TransitionedPos = vec4(aPos,0.0f).xyz; + + //TransitionedPos = (vec4(aPos,0.0f) *rotationX(radians(180.0f))).xyz; + //TransitionedPos = (vec4(aPos,0.0f) *rotationY(radians(90.0f))).xyz; + TransitionedPos = (vec4(aPos,0.0f) *rotationY(radians(-90.0f))).xyz; + break; + } + TextureIndex += float(tick); + + BiomeColor = aBiomeColor; + + gl_Position = projection * view * vec4(TransitionedPos + aOffset + vec3(0.5f,0.5f,0.5f), 1.0f); //Offseting by 0.5 to center the cube + //CubeSideIndex = CubeSide; //Passing cube side index to fragment shader +} diff --git a/prismarine-viewer/examples/newStats.ts b/prismarine-viewer/examples/newStats.ts new file mode 100644 index 000000000..fb363484f --- /dev/null +++ b/prismarine-viewer/examples/newStats.ts @@ -0,0 +1,35 @@ +let rightOffset = 0 + +const stats = {} + +export const addNewStat = (id: string, width = 80, x = rightOffset, y = 0) => { + const pane = document.createElement('div') + pane.id = 'fps-counter' + pane.style.position = 'fixed' + pane.style.top = `${y}px` + pane.style.right = `${x}px` + // gray bg + pane.style.backgroundColor = 'rgba(0, 0, 0, 0.5)' + pane.style.color = 'white' + pane.style.padding = '2px' + pane.style.fontFamily = 'monospace' + pane.style.fontSize = '12px' + pane.style.zIndex = '10000' + pane.style.pointerEvents = 'none' + document.body.appendChild(pane) + stats[id] = pane + if (y === 0) { // otherwise it's a custom position + rightOffset += width + } + + return { + updateText: (text: string) => { + pane.innerText = text + } + } +} + +export const updateStatText = (id, text) => { + if (!stats[id]) return + stats[id].innerText = text +} diff --git a/prismarine-viewer/examples/playground.ts b/prismarine-viewer/examples/playground.ts index a1bd6d0f1..a5c9f6720 100644 --- a/prismarine-viewer/examples/playground.ts +++ b/prismarine-viewer/examples/playground.ts @@ -12,10 +12,17 @@ import { loadScript } from '../viewer/lib/utils' import JSZip from 'jszip' import { TWEEN_DURATION } from '../viewer/lib/entities' import Entity from '../viewer/lib/entity/Entity' +// import * as Mathgl from 'math.gl' +import { findTextureInBlockStates } from '../../src/playerWindows' +import { initWebglRenderer, loadFixtureSides, setAnimationTick } from './webglRenderer' +import { renderToDom } from '@zardoy/react-util' globalThis.THREE = THREE //@ts-ignore import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { renderPlayground } from './TouchControls2' +import { WorldRendererWebgl } from '../viewer/lib/worldrendererWebgl' +import { TextureAnimation } from './TextureAnimation' const gui = new GUI() @@ -39,7 +46,8 @@ const params = { entityRotate: false, camera: '', playSound () { }, - blockIsomorphicRenderBundle () { } + blockIsomorphicRenderBundle () { }, + animationTick: 0 } const qs = new URLSearchParams(window.location.search) @@ -59,10 +67,20 @@ const setQs = () => { let ignoreResize = false +const enableControls = new URLSearchParams(window.location.search).get('controls') === 'true' + async function main () { let continuousRender = false - const { version } = params + // const { version } = params + let fixtureUrl = qs.get('fixture') + let fixture: undefined | Record + if (fixtureUrl) { + console.log('Loading fixture') + fixture = await fetch(fixtureUrl).then(r => r.json()) + console.log('Loaded fixture') + } + const version = fixture?.version ?? '1.20.2' // temporary solution until web worker is here, cache data for faster reloads const globalMcData = window['mcData'] if (!globalMcData['version']) { @@ -92,6 +110,7 @@ async function main () { gui.add(params, 'skip') gui.add(params, 'playSound') gui.add(params, 'blockIsomorphicRenderBundle') + const animationController = gui.add(params, 'animationTick', -1, 20, 1).listen() gui.open(false) let metadataFolder = gui.addFolder('metadata') // let entityRotationFolder = gui.addFolder('entity metadata') @@ -112,8 +131,12 @@ async function main () { const chunk1 = new Chunk() //@ts-ignore const chunk2 = new Chunk() - chunk1.setBlockStateId(targetPos, 34) - chunk2.setBlockStateId(targetPos.offset(1, 0, 0), 34) + chunk1.setBlockStateId(targetPos, 1) + chunk2.setBlockStateId(targetPos.offset(1, 0, 0), 1) + chunk1.setBlockStateId(targetPos.offset(0, 1, 1), 2) + // chunk1.setBlockStateId(targetPos.offset(0, 1, 0), 1) + // chunk1.setBlockStateId(targetPos.offset(1, 1, 0), 1) + // chunk1.setBlockStateId(targetPos.offset(-1, 1, 0), 1) const world = new World((chunkX, chunkZ) => { // if (chunkX === 0 && chunkZ === 0) return chunk1 // if (chunkX === 1 && chunkZ === 0) return chunk2 @@ -122,185 +145,126 @@ async function main () { return chunk }) + let stopUpdate = false + // let stopUpdate = true + // await schem.paste(world, new Vec3(0, 60, 0)) const worldView = new WorldDataEmitter(world, viewDistance, targetPos) - - // Create three.js context, add to page - const renderer = new THREE.WebGLRenderer({ alpha: true, ...localStorage['renderer'] }) - renderer.setPixelRatio(window.devicePixelRatio || 1) - renderer.setSize(window.innerWidth, window.innerHeight) - document.body.appendChild(renderer.domElement) - - // Create viewer - const viewer = new Viewer(renderer, 1) - viewer.entities.setDebugMode('basic') + const nullRenderer = new THREE.WebGLRenderer({ antialias: true }) + const viewer = new Viewer(nullRenderer, 1) + viewer.world.stopBlockUpdate = stopUpdate viewer.setVersion(version) - viewer.entities.onSkinUpdate = () => { - viewer.update() - viewer.render() - } - - viewer.listen(worldView) - // Load chunks - await worldView.init(targetPos) - window['worldView'] = worldView - window['viewer'] = viewer - - params.blockIsomorphicRenderBundle = () => { - const canvas = renderer.domElement - const onlyCurrent = !confirm('Ok - render all blocks, Cancel - render only current one') - const sizeRaw = prompt('Size', '512') - if (!sizeRaw) return - const size = parseInt(sizeRaw) - // const size = 512 - - ignoreResize = true - canvas.width = size - canvas.height = size - renderer.setSize(size, size) - - //@ts-ignore - viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10) - viewer.scene.background = null - - const rad = THREE.MathUtils.degToRad(-120) - viewer.directionalLight.position.set( - Math.cos(rad), - Math.sin(rad), - 0.2 - ).normalize() - viewer.directionalLight.intensity = 1 - - const cameraPos = targetPos.offset(2, 2, 2) - const pitch = THREE.MathUtils.degToRad(-30) - const yaw = THREE.MathUtils.degToRad(45) - viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX') - // viewer.camera.lookAt(center.x + 0.5, center.y + 0.5, center.z + 0.5) - viewer.camera.position.set(cameraPos.x + 1, cameraPos.y + 0.5, cameraPos.z + 1) - - const allBlocks = mcData.blocksArray.map(b => b.name) - // const allBlocks = ['stone', 'warped_slab'] - - let blockCount = 1 - let blockName = allBlocks[0] - - const updateBlock = () => { - - //@ts-ignore - // viewer.setBlockStateId(targetPos, mcData.blocksByName[blockName].minStateId) - params.block = blockName - // todo cleanup (introduce getDefaultState) - onUpdate.block() - applyChanges(false, true) - } - viewer.waitForChunksToRender().then(async () => { - // wait for next macro task - await new Promise(resolve => { - setTimeout(resolve, 0) - }) - if (onlyCurrent) { - viewer.render() - onWorldUpdate() - } else { - // will be called on every render update - viewer.world.renderUpdateEmitter.addListener('update', onWorldUpdate) - updateBlock() + globalThis.viewer = viewer + + await initWebglRenderer(version, () => { }, !enableControls && !fixture, true) + const simpleControls = () => { + let pressedKeys = new Set() + const loop = () => { + // Create a vector that points in the direction the camera is looking + let direction = new THREE.Vector3(0, 0, 0); + if (pressedKeys.has('KeyW')) { + direction.z = -0.5; + } + if (pressedKeys.has('KeyS')) { + direction.z += 0.5 + } + if (pressedKeys.has('KeyA')) { + direction.x -= 0.5 + } + if (pressedKeys.has('KeyD')) { + direction.x += 0.5 } - }) - - const zip = new JSZip() - zip.file('description.txt', 'Generated with prismarine-viewer') - - const end = async () => { - // download zip file - const a = document.createElement('a') - const blob = await zip.generateAsync({ type: 'blob' }) - const dataUrlZip = URL.createObjectURL(blob) - a.href = dataUrlZip - a.download = 'blocks_render.zip' - a.click() - URL.revokeObjectURL(dataUrlZip) - console.log('end') - viewer.world.renderUpdateEmitter.removeListener('update', onWorldUpdate) + if (pressedKeys.has('ShiftLeft')) { + viewer.camera.position.y -= 0.5 + } + if (pressedKeys.has('Space')) { + viewer.camera.position.y += 0.5 + } + direction.applyQuaternion(viewer.camera.quaternion); + direction.y = 0; + // Add the vector to the camera's position to move the camera + viewer.camera.position.add(direction); } - - async function onWorldUpdate () { - // await new Promise(resolve => { - // setTimeout(resolve, 50) - // }) - const dataUrl = canvas.toDataURL('image/png') - - zip.file(`${blockName}.png`, dataUrl.split(',')[1], { base64: true }) - - if (onlyCurrent) { - end() + setInterval(loop, 1000 / 30) + const keys = (e) => { + const code = e.code + const pressed = e.type === 'keydown' + if (pressed) { + pressedKeys.add(code) } else { - nextBlock() + pressedKeys.delete(code) } } - const nextBlock = async () => { - blockName = allBlocks[blockCount++] - console.log(allBlocks.length, '/', blockCount, blockName) - if (blockCount % 5 === 0) { - await new Promise(resolve => { - setTimeout(resolve, 100) - }) + window.addEventListener('keydown', keys) + window.addEventListener('keyup', keys) + + // mouse + const mouse = { x: 0, y: 0 } + const mouseMove = (e: PointerEvent) => { + if ((e.target as HTMLElement).closest('.lil-gui')) return + if (e.buttons === 1 || e.pointerType === 'touch') { + viewer.camera.rotation.x -= e.movementY / 100 + //viewer.camera. + viewer.camera.rotation.y -= e.movementX / 100 + if (viewer.camera.rotation.x < -Math.PI / 2) viewer.camera.rotation.x = -Math.PI / 2 + if (viewer.camera.rotation.x > Math.PI / 2) viewer.camera.rotation.x = Math.PI / 2 + + // yaw += e.movementY / 20; + // pitch += e.movementX / 20; } - if (blockName) { - updateBlock() - } else { - end() + if (e.buttons === 2) { + viewer.camera.position.set(0, 0, 0) } } + window.addEventListener('pointermove', mouseMove) + } + simpleControls() + renderPlayground() + + const writeToIndexedDb = async (name, data) => { + const db = await window.indexedDB.open(name, 1) + db.onupgradeneeded = (e) => { + const db = (e.target as any).result + db.createObjectStore(name) + } + db.onsuccess = (e) => { + const db = (e.target as any).result + const tx = db.transaction(name, 'readwrite') + const store = tx.objectStore(name) + store.add(data, name) + } } - // const jsonData = await fetch('https://bluecolored.de/bluemap/maps/overworld/tiles/0/x-2/2/z1/6.json?584662').then(r => r.json()) - - // const uniforms = { - // distance: { value: 0 }, - // sunlightStrength: { value: 1 }, - // ambientLight: { value: 0 }, - // skyColor: { value: new THREE.Color(0.5, 0.5, 1) }, - // voidColor: { value: new THREE.Color(0, 0, 0) }, - // hiresTileMap: { - // value: { - // map: null, - // size: 100, - // scale: new THREE.Vector2(1, 1), - // translate: new THREE.Vector2(), - // pos: new THREE.Vector2(), - // } - // } - - // } - - // const shader1 = new THREE.ShaderMaterial({ - // uniforms: uniforms, - // vertexShader: [0, 0, 0, 0], - // fragmentShader: fragmentShader, - // transparent: false, - // depthWrite: true, - // depthTest: true, - // vertexColors: true, - // side: THREE.FrontSide, - // wireframe: false - // }) + if (fixture) { + loadFixtureSides(fixture.sides) + const pos = fixture.camera[0] + viewer.camera.position.set(pos[0], pos[1], pos[2]) + } + + if (!enableControls) return + + // Create viewer + viewer.listen(worldView) + // Load chunks + await worldView.init(targetPos) + window['worldView'] = worldView + window['viewer'] = viewer //@ts-ignore - const controls = new OrbitControls(viewer.camera, renderer.domElement) - controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5) + // const controls = new OrbitControls(viewer.camera, nullRenderer.domElement) + // controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5) const cameraPos = targetPos.offset(2, 2, 2) const pitch = THREE.MathUtils.degToRad(-45) const yaw = THREE.MathUtils.degToRad(45) viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX') - viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5) - viewer.camera.position.set(cameraPos.x + 0.5, cameraPos.y + 0.5, cameraPos.z + 0.5) - controls.update() + // viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5) + viewer.camera.position.set(cameraPos.x, cameraPos.y, cameraPos.z) + // controls.update() let blockProps = {} let entityOverrides = {} @@ -315,7 +279,7 @@ async function main () { id: 'id', name: params.entity, pos: targetPos.offset(0.5, 1, 0.5), width: 1, height: 1, username: localStorage.testUsername, yaw: Math.PI, pitch: 0 }) const enableSkeletonDebug = (obj) => { - const {children, isSkeletonHelper} = obj + const { children, isSkeletonHelper } = obj if (!Array.isArray(children)) return if (isSkeletonHelper) { obj.visible = true @@ -332,6 +296,9 @@ async function main () { }, TWEEN_DURATION) } + params.block ||= 'stone' + + let textureAnimation: TextureAnimation | undefined const onUpdate = { block () { metadataFolder.destroy() @@ -398,6 +365,26 @@ async function main () { }, supportBlock () { viewer.setBlockStateId(targetPos.offset(0, -1, 0), params.supportBlock ? 1 : 0) + }, + animationTick () { + const webgl = (viewer.world as WorldRendererWebgl).playgroundGetWebglData() + if (!webgl?.animation) { + setAnimationTick(0) + return + } + if (params.animationTick === -1) { + textureAnimation = new TextureAnimation(new Proxy({} as any, { + set (t, p, v) { + if (p === 'tick') { + setAnimationTick(v) + } + return true + } + }), webgl.animation, webgl.animation.framesCount) + } else { + setAnimationTick(params.animationTick) + textureAnimation = undefined + } } } @@ -435,7 +422,9 @@ async function main () { if (object === params) { if (property === 'camera') return onUpdate[property]?.() - applyChanges(property === 'metadata') + if (property !== 'animationTick') { + applyChanges(property === 'metadata') + } } else { applyChanges() } @@ -448,26 +437,36 @@ async function main () { update() } applyChanges(true) - gui.openAnimated() + // gui.openAnimated() }) - const animate = () => { + const animate = () => { } + const animate2 = () => { // if (controls) controls.update() // worldView.updatePosition(controls.target) viewer.update() viewer.render() - // window.requestAnimationFrame(animate) + window.requestAnimationFrame(animate2) } viewer.world.renderUpdateEmitter.addListener('update', () => { - animate() + // const frames = viewer.world.hasWithFrames ? viewer.world.hasWithFrames - 1 : 0; + const webgl = (viewer.world as WorldRendererWebgl).playgroundGetWebglData() + if (webgl?.animation) { + params.animationTick = -1 + animationController.show() + animationController.max(webgl.animation.framesCount) + } else { + animationController.hide() + } + onUpdate.animationTick() }) - animate() + animate2() // #region camera rotation param if (params.camera) { const [x, y] = params.camera.split(',') viewer.camera.rotation.set(parseFloat(x), parseFloat(y), 0, 'ZYX') - controls.update() + // controls.update() console.log(viewer.camera.rotation.x, parseFloat(x)) } const throttledCamQsUpdate = _.throttle(() => { @@ -475,16 +474,16 @@ async function main () { // params.camera = `${camera.rotation.x.toFixed(2)},${camera.rotation.y.toFixed(2)}` setQs() }, 200) - controls.addEventListener('change', () => { - throttledCamQsUpdate() - animate() - }) + // controls.addEventListener('change', () => { + // throttledCamQsUpdate() + // animate() + // }) // #endregion + let time = performance.now() const continuousUpdate = () => { - if (continuousRender) { - animate() - } + textureAnimation?.step(performance.now() - time) + time = performance.now() requestAnimationFrame(continuousUpdate) } continuousUpdate() @@ -499,7 +498,7 @@ async function main () { const { camera } = viewer viewer.camera.aspect = window.innerWidth / window.innerHeight viewer.camera.updateProjectionMatrix() - renderer.setSize(window.innerWidth, window.innerHeight) + nullRenderer.setSize(window.innerWidth, window.innerHeight) animate() } diff --git a/prismarine-viewer/examples/shared.ts b/prismarine-viewer/examples/shared.ts new file mode 100644 index 000000000..318396fae --- /dev/null +++ b/prismarine-viewer/examples/shared.ts @@ -0,0 +1,11 @@ +export type BlockFaceType = { + face: number + textureIndex: number + textureName?: string + tint?: [number, number, number] + isTransparent?: boolean +} + +export type BlockType = { + sides: BlockFaceType[] +} diff --git a/prismarine-viewer/examples/webglRenderer.ts b/prismarine-viewer/examples/webglRenderer.ts new file mode 100644 index 000000000..1047a50f2 --- /dev/null +++ b/prismarine-viewer/examples/webglRenderer.ts @@ -0,0 +1,215 @@ +import { generateSpiralMatrix } from 'flying-squid/dist/utils' +import { Viewer } from '../viewer/lib/viewer' +import { options } from '../../src/optionsStorage' +import { addNewStat } from './newStats' + +let worker + +declare const viewer: Viewer + +export const sendWorkerMessage = (message: any, transfer?: Transferable[]) => { + worker.postMessage(message, transfer) + // replacable by onmessage +} + +let allReceived = false +declare const customEvents +declare const bot +if (typeof customEvents !== 'undefined') { + customEvents.on('gameLoaded', () => { + const chunksExpected = generateSpiralMatrix(options.renderDistance) + let received = 0 + bot.on('chunkColumnLoad', (data) => { + received++ + if (received === chunksExpected.length) { + allReceived = true + // addBlocksSection('all', viewer.world.newChunks) + } + }) + }) +} + + +let isWaitingToUpload = false +export const addBlocksSection = (key, data) => { + sendWorkerMessage({ + type: 'addBlocksSection', data, key + }) + if (playground && !isWaitingToUpload) { + isWaitingToUpload = true + // viewer.waitForChunksToRender().then(() => { + // isWaitingToUpload = false + // sendWorkerMessage({ + // type: 'addBlocksSectionDone' + // }) + // }) + } +} + +export const loadFixtureSides = (json) => { + sendWorkerMessage({ + type: 'loadFixture', json + }) +} + +export const sendCameraToWorker = () => { + const cameraData = ['rotation', 'position'].reduce((acc, key) => { + acc[key] = ['x', 'y', 'z'].reduce((acc2, key2) => { + acc2[key2] = viewer.camera[key][key2] + return acc2 + }, {}) + return acc + }, {}) + sendWorkerMessage({ + type: 'camera', + camera: cameraData + }) +} + +export const removeBlocksSection = (key) => { + sendWorkerMessage({ + type: 'removeBlocksSection', key + }) +} + +let playground = false +export const initWebglRenderer = async (version: string, postRender = () => { }, playgroundModeInWorker = false, actuallyPlayground = false) => { + playground = actuallyPlayground + await new Promise(resolve => { + // console.log('viewer.world.material.map!.image', viewer.world.material.map!.image) + // viewer.world.material.map!.image.onload = () => { + // console.log(this.material.map!.image) + // resolve() + // } + viewer.world.renderUpdateEmitter.once('blockStatesDownloaded', resolve) + }) + const imageBlob = await fetch(`./textures/${version}.png`).then((res) => res.blob()) + const canvas = document.createElement('canvas') + canvas.width = window.innerWidth * window.devicePixelRatio + canvas.height = window.innerHeight * window.devicePixelRatio + document.body.appendChild(canvas) + canvas.id = 'viewer-canvas' + + const offscreen = canvas.transferControlToOffscreen() + + // replacable by initWebglRenderer + worker = new Worker('./webglRendererWorker.js') + addFpsCounters() + sendWorkerMessage({ + canvas: offscreen, + imageBlob, + isPlayground: playgroundModeInWorker, + FragShaderOverride: localStorage.FragShader + }, [offscreen]) + + let oldWidth = window.innerWidth + let oldHeight = window.innerHeight + let oldCamera = { + position: { x: 0, y: 0, z: 0 }, + rotation: { x: 0, y: 0, z: 0 } + } + let focused = true + window.addEventListener('focus', () => { + focused = true + sendWorkerMessage({ type: 'startRender' }) + }) + window.addEventListener('blur', () => { + focused = false + sendWorkerMessage({ type: 'stopRender' }) + }) + const mainLoop = () => { + requestAnimationFrame(mainLoop) + //@ts-ignore + if (!focused || window.stopRender) return + + if (oldWidth !== window.innerWidth || oldHeight !== window.innerHeight) { + oldWidth = window.innerWidth + oldHeight = window.innerHeight + sendWorkerMessage({ + type: 'resize', + newWidth: window.innerWidth * window.devicePixelRatio, + height: window.innerHeight * window.devicePixelRatio + }) + } + postRender() + // TODO! do it in viewer to avoid possible delays + if (actuallyPlayground && ['rotation', 'position'].some((key) => oldCamera[key] !== viewer.camera[key])) { + // TODO fix + for (const [key, val] of Object.entries(oldCamera)) { + for (const key2 of Object.keys(val)) { + oldCamera[key][key2] = viewer.camera[key][key2] + } + } + sendCameraToWorker() + } + } + + requestAnimationFrame(mainLoop) +} + +export const setAnimationTick = (tick: number, frames?: number) => { + sendWorkerMessage({ + type: 'animationTick', + tick, + frames + }) +} + +globalThis.exportFixture = () => { + worker.postMessage({ type: 'exportData' }) + const controller = new AbortController() + worker.addEventListener('message', async (e) => { + const receivedData = e.data.data; + console.log('received fixture') + // await new Promise(resolve => { + // setTimeout(resolve, 0) + // }) + try { + const a = document.createElement('a') + type Vec3 = [number, number, number] + type PlayTimeline = [pos: Vec3, rot: Vec3, time: number] + const vec3ToArr = (vec3: { x, y, z }) => [vec3.x, vec3.y, vec3.z] as Vec3 + // const dataObj = { + // ...receivedData, + // version: viewer.version, + // camera: [vec3ToArr(viewer.camera.position), vec3ToArr(viewer.camera.rotation)], + // playTimeline: [] as PlayTimeline[] + // } + // split into two chunks + const objectURL = URL.createObjectURL(new Blob([receivedData.sides.buffer], { type: 'application/octet-stream' })) + a.href = objectURL + a.download = 'fixture.bin' + a.click() + URL.revokeObjectURL(objectURL) + } finally { + controller.abort() + } + }, { signal: controller.signal }) +} + + +const addFpsCounters = () => { + const { updateText } = addNewStat('fps') + let prevTimeout + worker.addEventListener('message', (e) => { + if (e.data.type === 'fps') { + updateText(`FPS: ${e.data.fps}`) + if (prevTimeout) clearTimeout(prevTimeout); + prevTimeout = setTimeout(() => { + updateText('') + }, 1002) + } + }) + + const { updateText: updateText2 } = addNewStat('fps-main', 90, 0, 20) + let updates = 0 + const mainLoop = () => { + requestAnimationFrame(mainLoop) + updates++ + } + mainLoop() + setInterval(() => { + updateText2(`Main Loop: ${updates}`) + updates = 0 + }, 1000) +} diff --git a/prismarine-viewer/examples/webglRendererWorker.ts b/prismarine-viewer/examples/webglRendererWorker.ts new file mode 100644 index 000000000..40f145dbb --- /dev/null +++ b/prismarine-viewer/examples/webglRendererWorker.ts @@ -0,0 +1,601 @@ +import * as THREE from 'three' + +//@ts-ignore +import VertShader from './_VertexShader.vert' +//@ts-ignore +import FragShader from './_FragmentShader.frag' +import { BlockFaceType, BlockType } from './shared' +import * as tweenJs from '@tweenjs/tween.js' + +let allSides = [] as ([number, number, number, BlockFaceType] | undefined)[] +let allSidesAdded = 0 +let needsSidesUpdate = false + +let chunksArrIndexes = {} +let freeArrayIndexes = [] as [number, number][] +let rendering = true +let sidePositions +let updateCubes: (startIndex: any, forceUpdate?) => void +let lastNotUpdatedIndex +let lastNotUpdatedArrSize +let animationTick = 0; + +const updateCubesWhenAvailable = (pos) => { + if (updateCubes) { + updateCubes(pos) + } else { + setTimeout(updateCubesWhenAvailable, 100) + } +} + +const camera = new THREE.PerspectiveCamera(75, 1 / 1, 0.1, 1000) + +let renderedFrames = 0 +setInterval(() => { + // console.log('FPS:', renderedFrames) + postMessage({ type: 'fps', fps: renderedFrames }) + renderedFrames = 0 +}, 1000) + +const updateSize = (width, height) => { + camera.aspect = width / height + camera.updateProjectionMatrix() +} + + +export const initWebglRenderer = async (canvas: HTMLCanvasElement, imageBlob: ImageBitmapSource, isPlayground: boolean, FragShaderOverride?) => { + // isPlayground = false + // blockStates = blockStatesJson + const textureBitmap = await createImageBitmap(imageBlob) + const textureWidth = textureBitmap.width + const textureHeight = textureBitmap.height + + const gl = canvas.getContext('webgl2')! + + const program = createProgram(gl, VertShader, FragShaderOverride || FragShader) + + let CubeMesh = new Float32Array([ + -0.5, -0.5, -0.5, 0.0, 0.0, 0.0, // Bottom-let + 0.5, -0.5, -0.5, 1.0, 0.0, 0.0, // bottom-right + 0.5, 0.5, -0.5, 1.0, 1.0, 0.0, // top-right + 0.5, 0.5, -0.5, 1.0, 1.0, 0.0, // top-right + -0.5, 0.5, -0.5, 0.0, 1.0, 0.0, // top-let + -0.5, -0.5, -0.5, 0.0, 0.0, 0.0, // bottom-let + // ront ace + -0.5, -0.5, 0.5, 0.0, 0.0, 1.0, // bottom-let + 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, // top-right + 0.5, -0.5, 0.5, 1.0, 0.0, 1.0, // bottom-right + 0.5, 0.5, 0.5, 1.0, 1.0, 1.0,// top-right + -0.5, -0.5, 0.5, 0.0, 0.0, 1.0,// bottom-let + -0.5, 0.5, 0.5, 0.0, 1.0, 1.0,// top-let + // Let ace + -0.5, 0.5, 0.5, 1.0, 0.0, 2.0,// top-right + -0.5, -0.5, -0.5, 0.0, 1.0, 2.0,// bottom-let + -0.5, 0.5, -0.5, 1.0, 1.0, 2.0,// top-let + -0.5, -0.5, -0.5, 0.0, 1.0, 2.0,// bottom-let + -0.5, 0.5, 0.5, 1.0, 0.0, 2.0, // top-right + -0.5, -0.5, 0.5, 0.0, 0.0, 2.0,// bottom-right + // Right ace + 0.5, 0.5, 0.5, 1.0, 0.0, 3.0,// top-let + 0.5, 0.5, -0.5, 1.0, 1.0, 3.0,// top-right + 0.5, -0.5, -0.5, 0.0, 1.0, 3.0,// bottom-right + 0.5, -0.5, -0.5, 0.0, 1.0, 3.0,// bottom-right + 0.5, -0.5, 0.5, 0.0, 0.0, 3.0,// bottom-let + 0.5, 0.5, 0.5, 1.0, 0.0, 3.0, // top-let + // Bottom ace + -0.5, -0.5, -0.5, 0.0, 1.0, 4.0,// top-right + 0.5, -0.5, 0.5, 1.0, 0.0, 4.0,// bottom-let + 0.5, -0.5, -0.5, 1.0, 1.0, 4.0,// top-let + 0.5, -0.5, 0.5, 1.0, 0.0, 4.0, // bottom-let + -0.5, -0.5, -0.5, 0.0, 1.0, 4.0, // top-right + -0.5, -0.5, 0.5, 0.0, 0.0, 4.0, // bottom-right + // Top ace + -0.5, 0.5, -0.5, 0.0, 1.0, 5.0,// top-let + 0.5, 0.5, -0.5, 1.0, 1.0, 5.0,// top-right + 0.5, 0.5, 0.5, 1.0, 0.0, 5.0,// bottom-right + 0.5, 0.5, 0.5, 1.0, 0.0, 5.0,// bottom-right + -0.5, 0.5, 0.5, 0.0, 0.0, 5.0,// bottom-let + -0.5, 0.5, -0.5, 0.0, 1.0, 5.0// top-let + ]) + + let SideMesh = new Float32Array([ + -0.5, -0.5, -0.5, 0.0, 0.0, // Bottom-let + 0.5, -0.5, -0.5, 1.0, 0.0, // bottom-right + -0.5, 0.5, -0.5, 0.0, 1.0, // top-let + 0.5, 0.5, -0.5, 1.0, 1.0, // top-right + // 0.5, 0.5, -0.5, 1.0, 1.0, // top-right + // -0.5, -0.5, -0.5, 0.0, 0.0, // bottom-let + // ront ace + ]) + + + + let NumberOfCube = isPlayground ? 10_000 : 16_000_000 + + sidePositions = new Float32Array(NumberOfCube * 3 * 6) + let sideTextureIndices = new Float32Array(NumberOfCube * 1 * 6); + let sideIndexes = new Float32Array(NumberOfCube * 1 * 6); + let sideBiomeColor = new Float32Array(NumberOfCube * 3 * 6); + + + // write random coordinates to cube positions xyz ten cubes; + if (isPlayground) { + for (let i = 0; i < NumberOfCube * 18; i += 18) { + + sidePositions[i] = Math.floor(Math.random() * 1000) - 500; + sidePositions[i + 1] = Math.floor(Math.random() * 1000) - 500; + sidePositions[i + 2] = Math.floor(Math.random() * 100) - 100; + + sideBiomeColor[i] = (Math.random()); + sideBiomeColor[i + 1] = (Math.random()); + sideBiomeColor[i + 2] = (Math.random()); + for (let j = 1; j <= 6; j++) { + + if (j != 6) { + sidePositions[j * 3 + i] = sidePositions[i] + sidePositions[j * 3 + i + 1] = sidePositions[i + 1] + sidePositions[j * 3 + i + 2] = sidePositions[i + 2] + + sideBiomeColor[j * 3 + i] = sideBiomeColor[i] + sideBiomeColor[j * 3 + i + 1] = sideBiomeColor[i + 1] + sideBiomeColor[j * 3 + i + 2] = sideBiomeColor[i + 2] + } + + sideIndexes[i / 3 + j - 1] = j - 1; + sideTextureIndices[i / 3 + j - 1] = Math.floor(Math.random() * 800); + //sideTextureIndices[i / 3 + j - 1] = 1; + } + + } + + } + let VAO = gl.createVertexArray(); + + + let instanceVBO = gl.createBuffer(); + let instanceTextureID = gl.createBuffer(); + let instanceBiomeColor = gl.createBuffer(); + let instanceCubeSide = gl.createBuffer(); + + gl.bindBuffer(gl.ARRAY_BUFFER, instanceVBO); + gl.bufferData(gl.ARRAY_BUFFER, sidePositions, gl.DYNAMIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + + gl.bindBuffer(gl.ARRAY_BUFFER, instanceTextureID); + gl.bufferData(gl.ARRAY_BUFFER, sideTextureIndices, gl.DYNAMIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + + gl.bindBuffer(gl.ARRAY_BUFFER, instanceBiomeColor); + gl.bufferData(gl.ARRAY_BUFFER, sideBiomeColor, gl.DYNAMIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + + gl.bindBuffer(gl.ARRAY_BUFFER, instanceCubeSide); + gl.bufferData(gl.ARRAY_BUFFER, sideIndexes, gl.DYNAMIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + VAO = gl.createVertexArray(); + let VBO = gl.createBuffer(); + // let VBO_sides = gl.createBuffer(); + //EBO = gl.createBuffer(); + + gl.bindVertexArray(VAO); + gl.bindBuffer(gl.ARRAY_BUFFER, VBO) + // gl.bindBuffer(gl.ARRAY_BUFFER, VBO_sides) + gl.bufferData(gl.ARRAY_BUFFER, SideMesh, gl.STATIC_DRAW) + + gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 5 * 4, 0) + gl.enableVertexAttribArray(0) + + gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 5 * 4, 3 * 4) + gl.enableVertexAttribArray(1) + + gl.enableVertexAttribArray(2); + gl.bindBuffer(gl.ARRAY_BUFFER, instanceCubeSide); + gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 4, 0); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.vertexAttribDivisor(2, 1); + + gl.enableVertexAttribArray(3); + gl.bindBuffer(gl.ARRAY_BUFFER, instanceVBO); + gl.vertexAttribPointer(3, 3, gl.FLOAT, false, 3 * 4, 0); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.vertexAttribDivisor(3, 1); + + gl.enableVertexAttribArray(4); + gl.bindBuffer(gl.ARRAY_BUFFER, instanceTextureID); + gl.vertexAttribPointer(4, 1, gl.FLOAT, false, 4 * 1, 0); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.vertexAttribDivisor(4, 1); + + gl.enableVertexAttribArray(6); + gl.bindBuffer(gl.ARRAY_BUFFER, instanceBiomeColor); + gl.vertexAttribPointer(6, 3, gl.FLOAT, false, 3 * 4, 0); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.vertexAttribDivisor(6, 1); + + updateCubes = (startIndex, forceUpdate) => { + // up2 + const newSides = allSides.slice(startIndex, lastNotUpdatedArrSize ? startIndex + lastNotUpdatedArrSize : undefined) + newSides.sort((a, b) => { + if (!a || !b) return 0 + const getScore = (b: BlockFaceType) => b.isTransparent ? 1 : 0 + return getScore(a[3]) - getScore(b[3]) + }) + globalThis.allSidesSize = allSides.length + sidePositions = new Float32Array(newSides.length * 3) + sideTextureIndices = new Float32Array(newSides.length * 1); + sideIndexes = new Float32Array(newSides.length * 1); + sideBiomeColor = new Float32Array(newSides.length * 3); + for (let i = 0; i < newSides.length * 3; i += 3) { + const newSide = newSides[i / 3]; + if (!newSide) continue + sidePositions[i] = newSide[0] + sidePositions[i + 1] = newSide[1] + sidePositions[i + 2] = newSide[2] + const block = newSide[3] as BlockFaceType + if (block.tint) { + const [r, g, b] = block.tint + sideBiomeColor[i] = r + sideBiomeColor[i + 1] = g + sideBiomeColor[i + 2] = b + } else { + sideBiomeColor[i] = 1 + sideBiomeColor[i + 1] = 1 + sideBiomeColor[i + 2] = 1 + } + sideTextureIndices[i / 3] = block.textureIndex + sideIndexes[i / 3] = block.face + } + + + // startIndex = 0 // TODO! + console.log('startIndex', startIndex, sidePositions.length, allSides.length) + const prepareBuffersUpdate = allSides.length > NumberOfCube || globalThis.testUpdate + globalThis.testUpdate = false + if (prepareBuffersUpdate) { + NumberOfCube += 1_000_000 + updateCubes(0, true) + return + } + globalThis.NumberOfCube = NumberOfCube + + const supplyData = (data, step) => { + if (forceUpdate) { + globalThis.updatedBufferSize = NumberOfCube + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(NumberOfCube * step), gl.STATIC_DRAW); + } + gl.bufferSubData(gl.ARRAY_BUFFER, startIndex * 4 * step, data); // update buffer content + const error = gl.getError() + if (error) throw new Error("SUBDATA ERROR") + gl.bindBuffer(gl.ARRAY_BUFFER, null); + } + + gl.bindBuffer(gl.ARRAY_BUFFER, instanceVBO); + supplyData(sidePositions, 3) + + gl.bindBuffer(gl.ARRAY_BUFFER, instanceTextureID); + supplyData(sideTextureIndices, 1) + + gl.bindBuffer(gl.ARRAY_BUFFER, instanceBiomeColor); + supplyData(sideBiomeColor, 3) + + gl.bindBuffer(gl.ARRAY_BUFFER, instanceCubeSide); + supplyData(sideIndexes, 1) + + allSidesAdded = allSides.length + needsSidesUpdate = true + lastNotUpdatedArrSize = undefined + } + + globalThis.updateCubes = updateCubes + globalThis.resetHalfScene = () => { + for (let i = 0; i < allSides.length / 2; i++) { + allSides[i] = undefined + } + lastNotUpdatedIndex = 0 + lastNotUpdatedArrSize = allSides.length / 2 + updateCubes(0) + } + const cleanupFirstChunks = () => { + allSides = [] + gl.bindBuffer(gl.ARRAY_BUFFER, instanceVBO); + // empty the buffer + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(NumberOfCube * 3), gl.STATIC_DRAW); // todo + gl.bindBuffer(gl.ARRAY_BUFFER, null); + + gl.bindBuffer(gl.ARRAY_BUFFER, instanceTextureID); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(NumberOfCube * 3), gl.STATIC_DRAW); // todo + // gl.bufferSubData(gl.ARRAY_BUFFER, startIndex * 4, cubeTextureIndices); // update buffer content + gl.bindBuffer(gl.ARRAY_BUFFER, null); + } + + fullReset = () => { + cleanupFirstChunks() + lastNotUpdatedIndex = undefined + lastNotUpdatedArrSize = undefined + } + + + + //gl.bindBuffer(gl.ARRAY_BUFFER, null); + //gl.bindVertexArray(null) + + // viewer.world.updateTexturesData() + // await new Promise(resolve => { + // // console.log('viewer.world.material.map!.image', viewer.world.material.map!.image) + // // viewer.world.material.map!.image.onload = () => { + // // console.log(this.material.map!.image) + // // resolve() + // // } + // viewer.world.renderUpdateEmitter.once('blockStatesDownloaded', resolve) + // }) + // const names = Object.keys(viewer.world.downloadedBlockStatesData) + + let texture1 = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture1); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, textureWidth, textureHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, textureBitmap); + + gl.useProgram(program) + + gl.uniform1i(gl.getUniformLocation(program, "texture1"), 0); + gl.uniform1i(gl.getUniformLocation(program, "texture2"), 1); + + + gl.enable(gl.DEPTH_TEST) + gl.frontFace(gl.CCW) + gl.enable(gl.CULL_FACE) + gl.enable(gl.BLEND) + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + + //gl.generateMipmap() + //gl.enable(gl) + //gl.clearColor(0, 0, 0, 1) + //gl.clear(gl.COLOR_BUFFER_BIT) + camera.up = new THREE.Vector3(0, 1, 0) + + let ViewUniform = gl.getUniformLocation(program, "view") + let ProjectionUniform = gl.getUniformLocation(program, "projection") + let TickUniform = gl.getUniformLocation(program, "tick") + + gl.cullFace(gl.FRONT) + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture1); + + gl.bindVertexArray(VAO) + gl.useProgram(program) + updateSize(gl.canvas.width, gl.canvas.height) + const renderLoop = (performance) => { + requestAnimationFrame(renderLoop) + if (!rendering && !needsSidesUpdate) return + // gl.canvas.width = window.innerWidth * window.devicePixelRatio + // gl.canvas.height = window.innerHeight * window.devicePixelRatio + if (newWidth || newHeight) { + gl.canvas.width = newWidth ?? gl.canvas.width + gl.canvas.height = newHeight ?? gl.canvas.height + newWidth = undefined + newHeight = undefined + updateSize(gl.canvas.width, gl.canvas.height) + + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height) + } + + + gl.clearColor(0.6784313725490196, 0.8470588235294118, 0.9019607843137255, 1); + gl.clear(gl.COLOR_BUFFER_BIT) + gl.clear(gl.DEPTH_BUFFER_BIT) + + + tweenJs.update() + camera.updateMatrix() + gl.uniformMatrix4fv(ViewUniform, false, camera.matrix.invert().elements); + gl.uniformMatrix4fv(ProjectionUniform, false, camera.projectionMatrix.elements); + gl.uniform1i(TickUniform, animationTick); + + if (!globalThis.stopRendering) { + gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, (isPlayground ? NumberOfCube * 6 : allSidesAdded)); + needsSidesUpdate = false + } + + renderedFrames++ + } + requestAnimationFrame(renderLoop) + + // gl.deleteVertexArray(VAO); + // gl.deleteBuffer(VBO) + // gl.deleteBuffer(EBO) + // gl.deleteProgram(program) + + return canvas +} + +let fullReset + +const createProgram = (gl: WebGL2RenderingContext, vertexShader: string, fragmentShader: string) => { + const createShader = (gl: WebGL2RenderingContext, type: number, source: string) => { + const shaderName = type === gl.VERTEX_SHADER ? 'vertex' : 'fragment' + const shader = gl.createShader(type)! + gl.shaderSource(shader, source) + gl.compileShader(shader) + + const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS) + if (!success) { + const info = gl.getShaderInfoLog(shader) + gl.deleteShader(shader) + throw new Error(`Shader ${shaderName} compile error: ` + info) + } + return shader + } + + + + const program = gl.createProgram()! + gl.attachShader(program, createShader(gl, gl.VERTEX_SHADER, vertexShader)!) + gl.attachShader(program, createShader(gl, gl.FRAGMENT_SHADER, fragmentShader)!) + gl.linkProgram(program) + const linkSuccess = gl.getProgramParameter(program, gl.LINK_STATUS) + if (!linkSuccess) { + const info = gl.getProgramInfoLog(program) + gl.deleteProgram(program) + throw new Error('Program link error: ' + info) + } + return program +} + +let started = false +let newWidth: number | undefined +let newHeight: number | undefined +let autoTickUpdate = undefined as number | undefined +onmessage = function (e) { + if (!started) { + started = true + initWebglRenderer(e.data.canvas, e.data.imageBlob, e.data.isPlayground, e.data.FragShaderOverride) + return + } + if (e.data.type === 'startRender') { + rendering = true + } + if (e.data.type === 'stopRender') { + rendering = false + } + if (e.data.type === 'resize') { + newWidth = e.data.newWidth + newHeight = e.data.newHeight + } + if (e.data.type === 'addBlocksSection') { + const currentLength = allSides.length; + // in: object - name, out: [x, y, z, name] + const newData = Object.entries(e.data.data.blocks).flatMap(([key, value]) => { + const [x, y, z] = key.split(',').map(Number) + const block = value as BlockType + return block.sides.map((side) => { + return [x, y, z, side] as [number, number, number, BlockFaceType] + }) + }) + // find freeIndexes if possible + const freeArea = freeArrayIndexes.find(([startIndex, endIndex]) => endIndex - startIndex >= newData.length) + if (freeArea) { + const [startIndex, endIndex] = freeArea + allSides.splice(startIndex, newData.length, ...newData) + lastNotUpdatedIndex ??= startIndex + const freeAreaIndex = freeArrayIndexes.indexOf(freeArea) + freeArrayIndexes[freeAreaIndex] = [startIndex + newData.length, endIndex] + if (freeArrayIndexes[freeAreaIndex][0] >= freeArrayIndexes[freeAreaIndex][1]) { + freeArrayIndexes.splice(freeAreaIndex, 1) + // todo merge + } + lastNotUpdatedArrSize = newData.length + console.log('using free area', freeArea) + } + + chunksArrIndexes[e.data.key] = [currentLength, currentLength + newData.length] + allSides.push(...newData) + lastNotUpdatedIndex ??= currentLength + // updateCubes?.(currentLength) + } + if (e.data.type === 'addBlocksSectionDone') { + // if (pendingUpdate) { + // console.log('Already pending') + // return + // } + updateCubesWhenAvailable(lastNotUpdatedIndex) + lastNotUpdatedIndex = undefined + lastNotUpdatedArrSize = undefined + } + if (e.data.type === 'removeBlocksSection') { + // fill data with 0 + const [startIndex, endIndex] = chunksArrIndexes[e.data.key] + for (let i = startIndex; i < endIndex; i++) { + allSides[i] = undefined + } + lastNotUpdatedArrSize = endIndex - startIndex + updateCubes(startIndex) + + // freeArrayIndexes.push([startIndex, endIndex]) + + // // merge freeArrayIndexes TODO + // if (freeArrayIndexes.at(-1)[0] === freeArrayIndexes.at(-2)?.[1]) { + // const [startIndex, endIndex] = freeArrayIndexes.pop()! + // const [startIndex2, endIndex2] = freeArrayIndexes.pop()! + // freeArrayIndexes.push([startIndex2, endIndex]) + // } + } + if (e.data.type === 'camera') { + camera.rotation.set(e.data.camera.rotation.x, e.data.camera.rotation.y, e.data.camera.rotation.z, 'ZYX') + // camera.position.set(e.data.camera.position.x, e.data.camera.position.y, e.data.camera.position.z) + if (camera.position.x === 0 && camera.position.y === 0 && camera.position.z === 0) { + // initial camera position + camera.position.set(e.data.camera.position.x, e.data.camera.position.y, e.data.camera.position.z) + } else { + new tweenJs.Tween(camera.position).to({ x: e.data.camera.position.x, y: e.data.camera.position.y, z: e.data.camera.position.z }, 50).start() + } + } + if (e.data.type === 'animationTick') { + if (e.data.frames <= 0) { + autoTickUpdate = undefined + animationTick = 0 + return + } + if (e.data.tick === -1) { + autoTickUpdate = e.data.frames + } else { + autoTickUpdate = undefined + animationTick = e.data.tick % 20 // todo update automatically in worker + } + } + if (e.data.type === 'fullReset') { + fullReset() + } + if (e.data.type === 'exportData') { + const exported = exportData(); + postMessage({ type: 'exportData', data: exported }, undefined, [exported.sides.buffer]) + } + if (e.data.type === 'loadFixture') { + // allSides = e.data.json.map(([x, y, z, face, textureIndex]) => { + // return [x, y, z, { face, textureIndex }] as [number, number, number, BlockFaceType] + // }) + const dataSize = e.data.json.length / 5 + for (let i = 0; i < e.data.json.length; i += 5) { + allSides.push([e.data.json[i], e.data.json[i + 1], e.data.json[i + 2], { face: e.data.json[i + 3], textureIndex: e.data.json[i + 4] }]) + } + updateCubesWhenAvailable(0) + } +} + +globalThis.testDuplicates = () => { + const duplicates = allSides.filter((arr, index, self) => { + return index !== self.findIndex((t) => { + return t[0] === arr[0] && t[1] === arr[1] && t[2] === arr[2] && t[3].face === arr[3].face + }) + }) + console.log('duplicates', duplicates) +} + +const exportData = () => { + // Calculate the total length of the final array + const totalLength = allSides.length * 5; + + // Create a new Int16Array with the total length + const flatData = new Int16Array(totalLength); + + // Fill the flatData array + for (let i = 0; i < allSides.length; i++) { + const [x, y, z, side] = allSides[i]; + flatData.set([x, y, z, side.face, side.textureIndex], i * 5); + } + + return { sides: flatData }; +} + +setInterval(() => { + if (autoTickUpdate) { + animationTick = (animationTick + 1) % autoTickUpdate; + } +}, 1000 / 20) diff --git a/prismarine-viewer/globals.d.ts b/prismarine-viewer/globals.d.ts new file mode 100644 index 000000000..c2cb5532a --- /dev/null +++ b/prismarine-viewer/globals.d.ts @@ -0,0 +1,9 @@ +type StringKeys = Extract + +interface ObjectConstructor { + keys (obj: T): Array> + entries (obj: T): Array<[StringKeys, T[keyof T]]> + // todo review https://stackoverflow.com/questions/57390305/trying-to-get-fromentries-type-right + fromEntries> (obj: T): Record + assign, K extends Record> (target: T, source: K): asserts target is T & K +} diff --git a/prismarine-viewer/package.json b/prismarine-viewer/package.json index 529aa9ae4..6942bb876 100644 --- a/prismarine-viewer/package.json +++ b/prismarine-viewer/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "@tweenjs/tween.js": "^20.0.3", + "live-server": "^1.2.2", "assert": "^2.0.0", "buffer": "^6.0.3", "canvas": "^2.11.2", diff --git a/prismarine-viewer/playground.html b/prismarine-viewer/playground.html index fd92009a4..c3902f40d 100644 --- a/prismarine-viewer/playground.html +++ b/prismarine-viewer/playground.html @@ -31,6 +31,7 @@ +
diff --git a/prismarine-viewer/sharedBuildOptions.mjs b/prismarine-viewer/sharedBuildOptions.mjs new file mode 100644 index 000000000..52b2fb8fc --- /dev/null +++ b/prismarine-viewer/sharedBuildOptions.mjs @@ -0,0 +1,3 @@ +export const sharedPlaygroundMainOptions = { + alias: {} +} \ No newline at end of file diff --git a/prismarine-viewer/tsconfig.json b/prismarine-viewer/tsconfig.json index 1cf48c6e3..421d76e33 100644 --- a/prismarine-viewer/tsconfig.json +++ b/prismarine-viewer/tsconfig.json @@ -1,9 +1,10 @@ { - "compilerOptions": { - "module": "commonjs", - "strictNullChecks": true - }, - "files": [ - "index.d.ts" - ] + "compilerOptions": { + "module": "commonjs", + "strictNullChecks": true + }, + "files": [ + "index.d.ts", + "globals.d.ts", + ] } diff --git a/prismarine-viewer/viewer/lib/models.ts b/prismarine-viewer/viewer/lib/models.ts index 6e43597a0..4c82b030e 100644 --- a/prismarine-viewer/viewer/lib/models.ts +++ b/prismarine-viewer/viewer/lib/models.ts @@ -2,9 +2,12 @@ import { Vec3 } from 'vec3' import { BlockStatesOutput } from '../prepare/modelsBuilder' import { World } from './world' import { Block } from 'prismarine-block' +import { BlockType } from '../../examples/shared' +import dataBlocks from '../lib/moreBlockDataGenerated.json' const tints: any = {} let blockStates: BlockStatesOutput +let textureSize: number let tintsData try { @@ -237,10 +240,229 @@ function buildRotationMatrix (axis, degree) { return matrix } -function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr, globalMatrix, globalShift, block: Block, biome) { - const cullIfIdentical = block.name.indexOf('glass') >= 0 +// function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr, globalMatrix, globalShift, block: Block, biome) { +// const cullIfIdentical = block.name.indexOf('glass') >= 0 + +// for (const face in element.faces) { +// const eFace = element.faces[face] +// const { corners, mask1, mask2 } = elemFaces[face] +// const dir = matmul3(globalMatrix, elemFaces[face].dir) + +// if (eFace.cullface) { +// const neighbor = world.getBlock(cursor.plus(new Vec3(...dir))) +// if (neighbor) { +// if (cullIfIdentical && neighbor.type === block.type) continue +// if (!neighbor.transparent && neighbor.isCube) continue +// } else { +// continue +// } +// } + +// const minx = element.from[0] +// const miny = element.from[1] +// const minz = element.from[2] +// const maxx = element.to[0] +// const maxy = element.to[1] +// const maxz = element.to[2] + +// const u = eFace.texture.u +// const v = eFace.texture.v +// const su = eFace.texture.su +// const sv = eFace.texture.sv + +// const ndx = Math.floor(attr.positions.length / 3) + +// let tint = [1, 1, 1] +// if (eFace.tintindex !== undefined) { +// if (eFace.tintindex === 0) { +// if (block.name === 'redstone_wire') { +// tint = tints.redstone[`${block.getProperties().power}`] +// } else if (block.name === 'birch_leaves' || +// block.name === 'spruce_leaves' || +// block.name === 'lily_pad') { +// tint = tints.constant[block.name] +// } else if (block.name.includes('leaves') || block.name === 'vine') { +// tint = tints.foliage[biome] +// } else { +// tint = tints.grass[biome] +// } +// } +// } + +// // UV rotation +// const r = eFace.rotation || 0 +// const uvcs = Math.cos(r * Math.PI / 180) +// const uvsn = -Math.sin(r * Math.PI / 180) + +// let localMatrix = null as any +// let localShift = null as any + +// if (element.rotation) { +// localMatrix = buildRotationMatrix( +// element.rotation.axis, +// element.rotation.angle +// ) + +// localShift = vecsub3( +// element.rotation.origin, +// matmul3( +// localMatrix, +// element.rotation.origin +// ) +// ) +// } + +// const aos: number[] = [] +// for (const pos of corners) { +// let vertex = [ +// (pos[0] ? maxx : minx), +// (pos[1] ? maxy : miny), +// (pos[2] ? maxz : minz) +// ] + +// vertex = vecadd3(matmul3(localMatrix, vertex), localShift) +// vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift) +// vertex = vertex.map(v => v / 16) + +// attr.positions.push( +// vertex[0] + (cursor.x & 15) - 8, +// vertex[1] + (cursor.y & 15) - 8, +// vertex[2] + (cursor.z & 15) - 8 +// ) + +// attr.normals.push(...dir) + +// const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5 +// const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5 +// attr.uvs.push(baseu * su + u, basev * sv + v) + +// let light = 1 +// if (doAO) { +// const dx = pos[0] * 2 - 1 +// const dy = pos[1] * 2 - 1 +// const dz = pos[2] * 2 - 1 +// const cornerDir = matmul3(globalMatrix, [dx, dy, dz]) +// const side1Dir = matmul3(globalMatrix, [dx * mask1[0], dy * mask1[1], dz * mask1[2]]) +// const side2Dir = matmul3(globalMatrix, [dx * mask2[0], dy * mask2[1], dz * mask2[2]]) +// const side1 = world.getBlock(cursor.offset(...side1Dir)) +// const side2 = world.getBlock(cursor.offset(...side2Dir)) +// const corner = world.getBlock(cursor.offset(...cornerDir)) + +// const side1Block = world.shouldMakeAo(side1) ? 1 : 0 +// const side2Block = world.shouldMakeAo(side2) ? 1 : 0 +// const cornerBlock = world.shouldMakeAo(corner) ? 1 : 0 + +// // TODO: correctly interpolate ao light based on pos (evaluate once for each corner of the block) + +// const ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock)) +// light = (ao + 1) / 4 +// aos.push(ao) +// } + +// attr.colors.push(tint[0] * light, tint[1] * light, tint[2] * light) +// } + +// if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) { +// attr.indices.push( +// ndx, ndx + 3, ndx + 2, +// ndx, ndx + 1, ndx + 3 +// ) +// } else { +// attr.indices.push( +// ndx, ndx + 1, ndx + 2, +// ndx + 2, ndx + 1, ndx + 3 +// ) +// } +// } +// } + +const facesIndexes = { + down: 0, + up: 1, + north: 2, + south: 3, + west: 4, + east: 5 +} + +type AttrType = { + blocks: { + [pos: string]: BlockType + } +} + +let textureName = undefined +let tint +const getResult = (biome, block, side: string): number => { + const facesOrTexture = findTextureInBlockStates(block.name); + if (!facesOrTexture) return 0 // todo + let result + if ('u' in facesOrTexture) { + result = facesOrTexture + } else { + result = facesOrTexture?.[side]?.texture + const tintindex = facesOrTexture?.[side]?.tintindex + if (tintindex === 0) { + if (block.name === 'redstone_wire') { + tint = tints.redstone[`${block.getProperties().power}`] + } else if (block.name === 'birch_leaves' || + block.name === 'spruce_leaves' || + block.name === 'lily_pad') { + tint = tints.constant[block.name] + } else if (block.name.includes('leaves') || block.name === 'vine') { + tint = tints.foliage[biome] + } else { + tint = tints.grass[biome] + } + } + } + if (!result) return 0 // todo + if (result.textureName) { + textureName = result.textureName + } + return uvToTextureIndex(result.u, result.v) - (result.su < 0 ? 1 : 0) - (result.sv < 0 ? 1 : 0) +} +function uvToTextureIndex (u, v) { + const textureWidth = textureSize + const textureHeight = textureSize + const tileSize = 16; + // Convert UV coordinates to pixel coordinates + let x = u * textureWidth; + let y = v * textureHeight; + + // Convert pixel coordinates to tile index + const tileX = Math.floor(x / tileSize); + const tileY = Math.floor(y / tileSize); + + // Calculate texture index + const textureIndex = tileY * (textureWidth / tileSize) + tileX; + + return textureIndex; +} + +const findTextureInBlockStates = (name): any => { + const vars = blockStates[name]?.variants + if (!vars) return blockStates[name]?.multipart?.[0]?.apply?.[0]?.model?.elements?.[0]?.faces?.south?.texture + let firstVar = Object.values(vars)[0] as any + if (Array.isArray(firstVar)) firstVar = firstVar[0] + if (!firstVar) return + const [element] = firstVar.model?.elements + if (!element) return firstVar.model?.textures?.particle + if (!element/* || !(element?.from.every(a => a === 0) && element?.to.every(a => a === 16)) */) return + return element.faces +} + +const isTransparent = (block: Block) => { + return block.transparent || block.material === 'plant' || block.name === 'water' || block.name === 'lava' + // return !!dataBlocks.noOcclusions[block.name] +} + +function renderElementNew (world: World, cursor: Vec3, element, doAO: boolean, attr: AttrType, globalMatrix, globalShift, block: Block, biome) { + const cullIfIdentical = block.name.indexOf('glass') >= 0 || block.name === 'water' || block.name === 'lava' for (const face in element.faces) { + const faceIndex = facesIndexes[face] + const eFace = element.faces[face] const { corners, mask1, mask2 } = elemFaces[face] const dir = matmul3(globalMatrix, elemFaces[face].dir) @@ -249,7 +471,7 @@ function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr const neighbor = world.getBlock(cursor.plus(new Vec3(...dir))) if (neighbor) { if (cullIfIdentical && neighbor.type === block.type) continue - if (!neighbor.transparent && neighbor.isCube) continue + if (!isTransparent(neighbor) && neighbor.isCube) continue } else { continue } @@ -267,9 +489,9 @@ function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr const su = eFace.texture.su const sv = eFace.texture.sv - const ndx = Math.floor(attr.positions.length / 3) + // const ndx = Math.floor(attr.positions.length / 3) - let tint = [1, 1, 1] + let tint = [1, 1, 1] as [number, number, number] if (eFace.tintindex !== undefined) { if (eFace.tintindex === 0) { if (block.name === 'redstone_wire') { @@ -309,67 +531,79 @@ function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr ) } - const aos: number[] = [] - for (const pos of corners) { - let vertex = [ - (pos[0] ? maxx : minx), - (pos[1] ? maxy : miny), - (pos[2] ? maxz : minz) - ] - - vertex = vecadd3(matmul3(localMatrix, vertex), localShift) - vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift) - vertex = vertex.map(v => v / 16) - - attr.positions.push( - vertex[0] + (cursor.x & 15) - 8, - vertex[1] + (cursor.y & 15) - 8, - vertex[2] + (cursor.z & 15) - 8 - ) - - attr.normals.push(...dir) - - const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5 - const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5 - attr.uvs.push(baseu * su + u, basev * sv + v) - - let light = 1 - if (doAO) { - const dx = pos[0] * 2 - 1 - const dy = pos[1] * 2 - 1 - const dz = pos[2] * 2 - 1 - const cornerDir = matmul3(globalMatrix, [dx, dy, dz]) - const side1Dir = matmul3(globalMatrix, [dx * mask1[0], dy * mask1[1], dz * mask1[2]]) - const side2Dir = matmul3(globalMatrix, [dx * mask2[0], dy * mask2[1], dz * mask2[2]]) - const side1 = world.getBlock(cursor.offset(...side1Dir)) - const side2 = world.getBlock(cursor.offset(...side2Dir)) - const corner = world.getBlock(cursor.offset(...cornerDir)) - - const side1Block = world.shouldMakeAo(side1) ? 1 : 0 - const side2Block = world.shouldMakeAo(side2) ? 1 : 0 - const cornerBlock = world.shouldMakeAo(corner) ? 1 : 0 - - // TODO: correctly interpolate ao light based on pos (evaluate once for each corner of the block) - - const ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock)) - light = (ao + 1) / 4 - aos.push(ao) - } - - attr.colors.push(tint[0] * light, tint[1] * light, tint[2] * light) - } - - if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) { - attr.indices.push( - ndx, ndx + 3, ndx + 2, - ndx, ndx + 1, ndx + 3 - ) - } else { - attr.indices.push( - ndx, ndx + 1, ndx + 2, - ndx + 2, ndx + 1, ndx + 3 - ) + const cursorPos = `${cursor.x},${cursor.y},${cursor.z}` + attr.blocks[cursorPos] ??= { + sides: [] } + attr.blocks[cursorPos].sides.push({ + face: faceIndex, + textureIndex: getResult(biome, block, face), + isTransparent: isTransparent(block), // todo + textureName, + tint + }) + + // const aos: number[] = [] + // for (const pos of corners) { + // let vertex = [ + // (pos[0] ? maxx : minx), + // (pos[1] ? maxy : miny), + // (pos[2] ? maxz : minz) + // ] + + // vertex = vecadd3(matmul3(localMatrix, vertex), localShift) + // vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift) + // vertex = vertex.map(v => v / 16) + + // attr.positions.push( + // vertex[0] + (cursor.x & 15) - 8, + // vertex[1] + (cursor.y & 15) - 8, + // vertex[2] + (cursor.z & 15) - 8 + // ) + + // attr.normals.push(...dir) + + // const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5 + // const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5 + // attr.uvs.push(baseu * su + u, basev * sv + v) + + // let light = 1 + // if (doAO) { + // const dx = pos[0] * 2 - 1 + // const dy = pos[1] * 2 - 1 + // const dz = pos[2] * 2 - 1 + // const cornerDir = matmul3(globalMatrix, [dx, dy, dz]) + // const side1Dir = matmul3(globalMatrix, [dx * mask1[0], dy * mask1[1], dz * mask1[2]]) + // const side2Dir = matmul3(globalMatrix, [dx * mask2[0], dy * mask2[1], dz * mask2[2]]) + // const side1 = world.getBlock(cursor.offset(...side1Dir)) + // const side2 = world.getBlock(cursor.offset(...side2Dir)) + // const corner = world.getBlock(cursor.offset(...cornerDir)) + + // const side1Block = world.shouldMakeAo(side1) ? 1 : 0 + // const side2Block = world.shouldMakeAo(side2) ? 1 : 0 + // const cornerBlock = world.shouldMakeAo(corner) ? 1 : 0 + + // // TODO: correctly interpolate ao light based on pos (evaluate once for each corner of the block) + + // const ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock)) + // light = (ao + 1) / 4 + // aos.push(ao) + // } + + // attr.colors.push(tint[0] * light, tint[1] * light, tint[2] * light) + // } + + // if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) { + // attr.indices.push( + // ndx, ndx + 3, ndx + 2, + // ndx, ndx + 1, ndx + 3 + // ) + // } else { + // attr.indices.push( + // ndx, ndx + 1, ndx + 2, + // ndx + 2, ndx + 1, ndx + 3 + // ) + // } } } @@ -388,7 +622,8 @@ export function getSectionGeometry (sx, sy, sz, world: World) { t_uvs: [], indices: [], // todo this can be removed here - signs: {} + signs: {}, + blocks: {} } as Record const cursor = new Vec3(0, 0, 0) @@ -416,33 +651,72 @@ export function getSectionGeometry (sx, sy, sz, world: World) { block.variant = getModelVariants(block) } + if (block.name === 'water' || block.name === 'lava') { + const textureParticle = block.variant![0].model.textures.particle + const tex = { texture: textureParticle, cullface: true, tintindex: 0 }; + block.variant![0].model.elements.push({ + from: [0, 0, 0], + to: [16, 16, 16], + faces: { + down: tex, + up: tex, + north: tex, + south: tex, + west: tex, + east: tex + } + }) + } + // if (block.name === 'water') { + // renderLiquid(world, cursor, variant.model.textures.particle, block.type, biome, true, attr) + // } else if (block.name === 'lava') { + // renderLiquid(world, cursor, variant.model.textures.particle, block.type, biome, false, attr) + // } else { + // let globalMatrix = null as any + // let globalShift = null as any + + // for (const axis of ['x', 'y', 'z']) { + // if (axis in variant) { + // if (!globalMatrix) globalMatrix = buildRotationMatrix(axis, -variant[axis]) + // else globalMatrix = matmulmat3(globalMatrix, buildRotationMatrix(axis, -variant[axis])) + // } + // } + + // if (globalMatrix) { + // globalShift = [8, 8, 8] + // globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift)) + // } + + // for (const element of variant.model.elements) { + // renderElement(world, cursor, element, variant.model.ao, attr, globalMatrix, globalShift, block, biome) + // } + // } + for (const variant of block.variant) { if (!variant || !variant.model) continue - if (block.name === 'water') { - renderLiquid(world, cursor, variant.model.textures.particle, block.type, biome, true, attr) - } else if (block.name === 'lava') { - renderLiquid(world, cursor, variant.model.textures.particle, block.type, biome, false, attr) - } else { - let globalMatrix = null as any - let globalShift = null as any - - for (const axis of ['x', 'y', 'z']) { - if (axis in variant) { - if (!globalMatrix) globalMatrix = buildRotationMatrix(axis, -variant[axis]) - else globalMatrix = matmulmat3(globalMatrix, buildRotationMatrix(axis, -variant[axis])) - } - } - - if (globalMatrix) { - globalShift = [8, 8, 8] - globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift)) + // if (block.name === 'water') { + // renderLiquid(world, cursor, variant.model.textures.particle, block.type, biome, true, attr) + //} else if (block.name === 'lava') { + // renderLiquid(world, cursor, variant.model.textures.particle, block.type, biome, false, attr) + let globalMatrix = null as any + let globalShift = null as any + + for (const axis of ['x', 'y', 'z']) { + if (axis in variant) { + if (!globalMatrix) globalMatrix = buildRotationMatrix(axis, -variant[axis]) + else globalMatrix = matmulmat3(globalMatrix, buildRotationMatrix(axis, -variant[axis])) } + } - for (const element of variant.model.elements) { - renderElement(world, cursor, element, variant.model.ao, attr, globalMatrix, globalShift, block, biome) - } + if (globalMatrix) { + globalShift = [8, 8, 8] + globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift)) } + + const elements = variant.model.elements; + const element = elements[0] + if (element) renderElementNew(world, cursor, element, variant.model.ao, attr as any, globalMatrix, globalShift, block, biome) } } } @@ -474,6 +748,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) { attr.normals = new Float32Array(attr.normals) as any attr.colors = new Float32Array(attr.colors) as any attr.uvs = new Float32Array(attr.uvs) as any + if (Object.keys(attr.blocks).length === 0) return attr return attr } @@ -536,6 +811,7 @@ function getModelVariants (block: import('prismarine-block').Block) { return [] } -export const setBlockStates = (_blockStates: BlockStatesOutput | null) => { +export const setRendererData = (_blockStates: BlockStatesOutput | null, _textureSize: number) => { + textureSize = _textureSize blockStates = _blockStates! } diff --git a/prismarine-viewer/viewer/lib/utils.web.js b/prismarine-viewer/viewer/lib/utils.web.js index 72d715b90..cbb942225 100644 --- a/prismarine-viewer/viewer/lib/utils.web.js +++ b/prismarine-viewer/viewer/lib/utils.web.js @@ -2,9 +2,11 @@ const THREE = require('three') const textureCache = {} -function loadTexture (texture, cb) { +function loadTexture (texture, cb, onLoad) { if (!textureCache[texture]) { - textureCache[texture] = new THREE.TextureLoader().load(texture) + textureCache[texture] = new THREE.TextureLoader().load(texture, onLoad) + } else { + onLoad?.() } cb(textureCache[texture]) } diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts index f58a7ba3c..01864cb3c 100644 --- a/prismarine-viewer/viewer/lib/viewer.ts +++ b/prismarine-viewer/viewer/lib/viewer.ts @@ -1,19 +1,22 @@ import * as THREE from 'three' import * as tweenJs from '@tweenjs/tween.js' import { Vec3 } from 'vec3' -import { WorldRenderer } from './worldrenderer' +import { WorldRendererWebgl } from './worldrendererWebgl' import { Entities } from './entities' import { Primitives } from './primitives' import { getVersion } from './version' import EventEmitter from 'events' import { EffectComposer, RenderPass, ShaderPass, FXAAShader } from 'three-stdlib' +import { sendCameraToWorker } from '../../examples/webglRenderer' +import { WorldRendererThree } from './worldrendererThree' +import { generateSpiralMatrix } from 'flying-squid/dist/utils' export class Viewer { scene: THREE.Scene ambientLight: THREE.AmbientLight directionalLight: THREE.DirectionalLight camera: THREE.PerspectiveCamera - world: WorldRenderer + world: WorldRendererWebgl/* | WorldRendererThree */ entities: Entities primitives: Primitives domElement: HTMLCanvasElement @@ -28,7 +31,7 @@ export class Viewer { fxaaPass: ShaderPass renderPass: RenderPass - constructor(public renderer: THREE.WebGLRenderer, numWorkers?: number, public enableFXAA = false) { + constructor (public renderer: THREE.WebGLRenderer, numWorkers?: number, public enableFXAA = false) { // https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791 THREE.ColorManagement.enabled = false renderer.outputColorSpace = THREE.LinearSRGBColorSpace @@ -39,7 +42,7 @@ export class Viewer { if (this.enableFXAA) { this.enableFxaaScene() } - this.world = new WorldRenderer(this.scene, numWorkers) + this.world = new WorldRendererWebgl(numWorkers) this.entities = new Entities(this.scene) this.primitives = new Primitives(this.scene, this.camera) @@ -112,9 +115,11 @@ export class Viewer { if (pos) { let y = pos.y + this.playerHeight if (this.isSneaking) y -= 0.3 - new tweenJs.Tween(cam.position).to({ x: pos.x, y, z: pos.z }, 50).start() + // new tweenJs.Tween(cam.position).to({ x: pos.x, y, z: pos.z }, 50).start() + cam.position.set(pos.x, y, pos.z) } cam.rotation.set(pitch, yaw, roll, 'ZYX') + sendCameraToWorker() } playSound (position: Vec3, path: string, volume = 1) { @@ -160,7 +165,7 @@ export class Viewer { }) // todo remove and use other architecture instead so data flow is clear emitter.on('blockEntities', (blockEntities) => { - this.world.blockEntities = blockEntities + (this.world as unknown as WorldRendererThree).blockEntities = blockEntities }) emitter.on('unloadChunk', ({ x, z }) => { @@ -175,6 +180,11 @@ export class Viewer { this.world.updateViewerPosition(pos) }) + emitter.on('renderDistance', (d) => { + this.world.viewDistance = d + this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length + }) + emitter.emit('listening') this.domElement.addEventListener?.('pointerdown', (evt) => { @@ -189,17 +199,17 @@ export class Viewer { } update () { - tweenJs.update() + // tweenJs.update() } render () { - if (this.composer) { - this.renderPass.camera = this.camera - this.composer.render() - } else { - this.renderer.render(this.scene, this.camera) - } - this.entities.render() + // if (this.composer) { + // this.renderPass.camera = this.camera + // this.composer.render() + // } else { + // this.renderer.render(this.scene, this.camera) + // } + // this.entities.render() } async waitForChunksToRender () { diff --git a/prismarine-viewer/viewer/lib/viewerWrapper.ts b/prismarine-viewer/viewer/lib/viewerWrapper.ts new file mode 100644 index 000000000..7e489a6c5 --- /dev/null +++ b/prismarine-viewer/viewer/lib/viewerWrapper.ts @@ -0,0 +1,117 @@ +// wrapper for now +export class ViewerWrapper { + previousWindowWidth: number + previousWindowHeight: number + globalObject = globalThis as any + stopRenderOnBlur = true + addedToPage = false + renderInterval = 0 + fpsInterval + + constructor(public canvas: HTMLCanvasElement, public renderer?: THREE.WebGLRenderer) { + } + addToPage (startRendering = true) { + if (this.addedToPage) throw new Error('Already added to page') + let pixelRatio = window.devicePixelRatio || 1 // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable + if (this.renderer) { + if (!this.renderer.capabilities.isWebGL2) pixelRatio = 1 // webgl1 has issues with high pixel ratio (sometimes screen is clipped) + this.renderer.setPixelRatio(pixelRatio) + this.renderer.setSize(window.innerWidth, window.innerHeight) + } else { + this.canvas.width = window.innerWidth * pixelRatio + this.canvas.height = window.innerHeight * pixelRatio + } + this.previousWindowWidth = window.innerWidth + this.previousWindowHeight = window.innerHeight + + this.canvas.id = 'viewer-canvas' + document.body.appendChild(this.canvas) + + if (this.renderer) this.globalObject.renderer = this.renderer + this.addedToPage = true + + let max = 0 + this.fpsInterval = setInterval(() => { + if (max > 0) { + viewer.world.droppedFpsPercentage = this.renderedFps / max + } + max = Math.max(this.renderedFps, max) + this.renderedFps = 0 + }, 1000) + if (startRendering) { + this.globalObject.requestAnimationFrame(this.render.bind(this)) + } + if (typeof window !== 'undefined') { + this.trackWindowFocus() + } + } + + windowFocused = true + trackWindowFocus () { + window.addEventListener('focus', () => { + this.windowFocused = true + }) + window.addEventListener('blur', () => { + this.windowFocused = false + }) + } + + dispose () { + if (!this.addedToPage) throw new Error('Not added to page') + document.body.removeChild(this.canvas) + this.renderer?.dispose() + // this.addedToPage = false + clearInterval(this.fpsInterval) + } + + + renderedFps = 0 + lastTime = performance.now() + delta = 0 + startRender = () => { } + endRender = () => { } + render (time: DOMHighResTimeStamp) { + if (this.globalObject.stopLoop) return + for (const fn of beforeRenderFrame) fn() + this.globalObject.requestAnimationFrame(this.render.bind(this)) + if (this.globalObject.stopRender || this.renderer?.xr.isPresenting || (this.stopRenderOnBlur && !this.windowFocused)) return + if (this.renderInterval) { + this.delta += time - this.lastTime + this.lastTime = time + if (this.delta > this.renderInterval) { + this.delta %= this.renderInterval + // continue rendering + } else { + return + } + } + // ios bug: viewport dimensions are updated after the resize event + if (this.previousWindowWidth !== window.innerWidth || this.previousWindowHeight !== window.innerHeight) { + this.resizeHandler() + this.previousWindowWidth = window.innerWidth + this.previousWindowHeight = window.innerHeight + } + this.startRender() + viewer.update() + viewer.render() + this.renderedFps++ + this.endRender() + } + + resizeHandler () { + const width = window.innerWidth + const height = window.innerHeight + + viewer.camera.aspect = width / height + viewer.camera.updateProjectionMatrix() + + if (this.renderer) { + this.renderer.setSize(width, height) + } + // canvas updated by renderer + + if (viewer.composer) { + viewer.updateComposerSize() + } + } +} diff --git a/prismarine-viewer/viewer/lib/worker.js b/prismarine-viewer/viewer/lib/worker.js index e1ef45c33..bcc2b06ca 100644 --- a/prismarine-viewer/viewer/lib/worker.js +++ b/prismarine-viewer/viewer/lib/worker.js @@ -1,3 +1,4 @@ +//@ts-check /* global postMessage self */ if (!global.self) { @@ -12,10 +13,11 @@ if (!global.self) { const { Vec3 } = require('vec3') const { World } = require('./world') -const { getSectionGeometry, setBlockStates } = require('./models') +const { getSectionGeometry, setRendererData } = require('./models') let world = null -let dirtySections = {} +/** @type {Map} */ +let dirtySections = new Map() let blockStatesReady = false function sectionKey (x, y, z) { @@ -26,24 +28,31 @@ function setSectionDirty (pos, value = true) { const x = Math.floor(pos.x / 16) * 16 const y = Math.floor(pos.y / 16) * 16 const z = Math.floor(pos.z / 16) * 16 - const chunk = world.getColumn(x, z) const key = sectionKey(x, y, z) if (!value) { - delete dirtySections[key] + dirtySections.delete(key) postMessage({ type: 'sectionFinished', key }) - } else if (chunk?.getSection(pos)) { - dirtySections[key] = value + return + } + + const chunk = world.getColumn(x, z); + if (chunk?.getSection(pos)) { + dirtySections.set(key, (dirtySections.get(key) || 0) + 1) } else { postMessage({ type: 'sectionFinished', key }) } } self.onmessage = ({ data }) => { + /** @type {any} */ + const globalVar = globalThis; + if (data.type === 'mcData') { - globalThis.mcData = data.mcData + globalVar.mcData = data.mcData world = new World(data.version) - } else if (data.type === 'blockStates') { - setBlockStates(data.json) + } else if (data.type === 'rendererData') { + setRendererData(data.json, data.textureSize) + world.outputFormat = data.outputFormat ?? world.outputFormat blockStatesReady = true } else if (data.type === 'dirty') { const loc = new Vec3(data.x, data.y, data.z) @@ -57,35 +66,38 @@ self.onmessage = ({ data }) => { world.setBlockStateId(loc, data.stateId) } else if (data.type === 'reset') { world = null - blocksStates = null - dirtySections = {} + // blocksStates = null + dirtySections = new Map() // todo also remove cached - globalThis.mcData = null + globalVar.mcData = null blockStatesReady = false } } setInterval(() => { if (world === null || !blockStatesReady) return - const sections = Object.keys(dirtySections) - if (sections.length === 0) return + if (dirtySections.size === 0) return // console.log(sections.length + ' dirty sections') // const start = performance.now() - for (const key of sections) { - let [x, y, z] = key.split(',') - x = parseInt(x, 10) - y = parseInt(y, 10) - z = parseInt(z, 10) + for (const key of dirtySections.keys()) { + let [x, y, z] = key.split(',').map(v => parseInt(v, 10)) const chunk = world.getColumn(x, z) if (chunk?.getSection(new Vec3(x, y, z))) { - delete dirtySections[key] const geometry = getSectionGeometry(x, y, z, world) const transferable = [geometry.positions.buffer, geometry.normals.buffer, geometry.colors.buffer, geometry.uvs.buffer] + //@ts-ignore postMessage({ type: 'geometry', key, geometry }, transferable) + } else { + console.info('[mesher] Missing section', x, y, z) } - postMessage({ type: 'sectionFinished', key }) + const dirtyTimes = dirtySections.get(key); + if (!dirtyTimes) throw new Error('dirtySections.get(key) is falsy') + for (let i = 0; i < dirtyTimes; i++) { + postMessage({ type: 'sectionFinished', key }) + } + dirtySections.delete(key) } // const time = performance.now() - start // console.log(`Processed ${sections.length} sections in ${time} ms (${time / sections.length} ms/section)`) diff --git a/prismarine-viewer/viewer/lib/world.ts b/prismarine-viewer/viewer/lib/world.ts index a4ffd69c6..9aa5311a4 100644 --- a/prismarine-viewer/viewer/lib/world.ts +++ b/prismarine-viewer/viewer/lib/world.ts @@ -18,6 +18,7 @@ function posInChunk (pos) { } function isCube (shapes) { + return true if (!shapes || shapes.length !== 1) return false const shape = shapes[0] return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 @@ -30,6 +31,7 @@ export type WorldBlock = Block & { } export class World { + outputFormat = 'threeJs' as 'threeJs' | 'webgl' Chunk: any/* import('prismarine-chunk/types/index').PCChunk */ columns = {} blockCache = {} diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts index fb17a9942..540b554ad 100644 --- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts +++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts @@ -36,6 +36,11 @@ export class WorldDataEmitter extends EventEmitter { }) } + updateViewDistance (viewDistance: number) { + this.viewDistance = viewDistance + this.emitter.emit('renderDistance', viewDistance) + } + listenToBot (bot: typeof __type_bot) { const emitEntity = (e) => { if (!e || e === bot.entity) return @@ -73,6 +78,7 @@ export class WorldDataEmitter extends EventEmitter { return bot.world.getBlock(new Vec3(x, y, z)).entity }, })) + this.emitter.emit('renderDistance', this.viewDistance) }) // node.js stream data event pattern if (this.emitter.listenerCount('blockEntities')) { diff --git a/prismarine-viewer/viewer/lib/worldrenderer.ts b/prismarine-viewer/viewer/lib/worldrenderer.ts deleted file mode 100644 index a3c0f79c0..000000000 --- a/prismarine-viewer/viewer/lib/worldrenderer.ts +++ /dev/null @@ -1,373 +0,0 @@ -import * as THREE from 'three' -import { Vec3 } from 'vec3' -import { loadTexture, loadJSON } from './utils' -import { EventEmitter } from 'events' -import mcDataRaw from 'minecraft-data/data.js' // handled correctly in esbuild plugin -import nbt from 'prismarine-nbt' -import { dynamicMcDataFiles } from '../../buildWorkerConfig.mjs' -import { dispose3 } from './dispose' -import { toMajor } from './version.js' -import PrismarineChatLoader from 'prismarine-chat' -import { renderSign } from '../sign-renderer/' -import { chunkPos, sectionPos } from './simpleUtils' - -function mod (x, n) { - return ((x % n) + n) % n -} - -export class WorldRenderer { - worldConfig = { minY: 0, worldHeight: 256 } - // todo @sa2urami set alphaTest back to 0.1 and instead properly sort transparent and solid objects (needs to be done in worker too) - material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.5 }) - - blockEntities = {} - sectionObjects: Record = {} - showChunkBorders = false - active = false - version = undefined as string | undefined - chunkTextures = new Map() - loadedChunks = {} - sectionsOutstanding = new Set() - renderUpdateEmitter = new EventEmitter() - customBlockStatesData = undefined as any - customTexturesDataUrl = undefined as string | undefined - downloadedBlockStatesData = undefined as any - downloadedTextureImage = undefined as any - workers: any[] = [] - viewerPosition?: Vec3 - lastCamUpdate = 0 - droppedFpsPercentage = 0 - initialChunksLoad = true - enableChunksLoadDelay = false - - texturesVersion?: string - - promisesQueue = [] as Promise[] - - constructor(public scene: THREE.Scene, numWorkers = 4) { - // init workers - for (let i = 0; i < numWorkers; i++) { - // Node environment needs an absolute path, but browser needs the url of the file - let src = __dirname - if (typeof window === 'undefined') src += '/worker.js' - else src = 'worker.js' - - const worker: any = new Worker(src) - worker.onmessage = async ({ data }) => { - if (!this.active) return - await new Promise(resolve => { - setTimeout(resolve, 0) - }) - if (data.type === 'geometry') { - let object: THREE.Object3D = this.sectionObjects[data.key] - if (object) { - this.scene.remove(object) - dispose3(object) - delete this.sectionObjects[data.key] - } - - const chunkCoords = data.key.split(',') - if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return - - // if (!this.initialChunksLoad && this.enableChunksLoadDelay) { - // const newPromise = new Promise(resolve => { - // if (this.droppedFpsPercentage > 0.5) { - // setTimeout(resolve, 1000 / 50 * this.droppedFpsPercentage) - // } else { - // setTimeout(resolve) - // } - // }) - // this.promisesQueue.push(newPromise) - // for (const promise of this.promisesQueue) { - // await promise - // } - // } - - const geometry = new THREE.BufferGeometry() - geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3)) - geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3)) - geometry.setAttribute('color', new THREE.BufferAttribute(data.geometry.colors, 3)) - geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2)) - geometry.setIndex(data.geometry.indices) - - const mesh = new THREE.Mesh(geometry, this.material) - mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz) - mesh.name = 'mesh' - object = new THREE.Group() - object.add(mesh) - const boxHelper = new THREE.BoxHelper(mesh, 0xffff00) - boxHelper.name = 'helper' - object.add(boxHelper) - object.name = 'chunk' - if (!this.showChunkBorders) { - boxHelper.visible = false - } - // should not compute it once - if (Object.keys(data.geometry.signs).length) { - for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.signs)) { - const [x, y, z] = posKey.split(',') - const signBlockEntity = this.blockEntities[posKey] - if (!signBlockEntity) continue - const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(signBlockEntity)); - if (!sign) continue - object.add(sign) - } - } - this.sectionObjects[data.key] = object - this.updatePosDataChunk(data.key) - this.scene.add(object) - } else if (data.type === 'sectionFinished') { - this.sectionsOutstanding.delete(data.key) - this.renderUpdateEmitter.emit('update') - } - } - if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) }) - this.workers.push(worker) - } - } - - /** - * Optionally update data that are depedendent on the viewer position - */ - updatePosDataChunk (key: string) { - if (!this.viewerPosition) return - const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16)) - const [xPlayer, yPlayer, zPlayer] = this.viewerPosition.toArray().map(x => Math.floor(x / 16)) - // sum of distances: x + y + z - const chunkDistance = Math.abs(x - xPlayer) + Math.abs(y - yPlayer) + Math.abs(z - zPlayer) - const section = this.sectionObjects[key].children.find(child => child.name === 'mesh')! - section.renderOrder = 500 - chunkDistance - } - - updateViewerPosition (pos: Vec3) { - this.viewerPosition = pos - for (const key of Object.keys(this.sectionObjects)) { - this.updatePosDataChunk(key) - } - } - - signsCache = new Map() - - getSignTexture (position: Vec3, blockEntity, backSide = false) { - const chunk = chunkPos(position) - let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`) - if (!textures) { - textures = {} - this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures) - } - const texturekey = `${position.x},${position.y},${position.z}`; - // todo investigate bug and remove this so don't need to clean in section dirty - if (textures[texturekey]) return textures[texturekey] - - const PrismarineChat = PrismarineChatLoader(this.version!) - const canvas = renderSign(blockEntity, PrismarineChat) - if (!canvas) return - const tex = new THREE.Texture(canvas) - tex.magFilter = THREE.NearestFilter - tex.minFilter = THREE.NearestFilter - tex.needsUpdate = true - textures[texturekey] = tex - return tex - } - - renderSign (position: Vec3, rotation: number, isWall: boolean, blockEntity) { - const tex = this.getSignTexture(position, blockEntity) - - if (!tex) return - - // todo implement - // const key = JSON.stringify({ position, rotation, isWall }) - // if (this.signsCache.has(key)) { - // console.log('cached', key) - // } else { - // this.signsCache.set(key, tex) - // } - - const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true, })) - mesh.renderOrder = 999 - - // todo @sa2urami shouldnt all this be done in worker? - mesh.scale.set(1, 7 / 16, 1) - if (isWall) { - mesh.position.set(0, 0, -(8 - 1.5) / 16 + 0.001) - } else { - // standing - const faceEnd = 8.75 - mesh.position.set(0, 0, (faceEnd - 16 / 2) / 16 + 0.001) - } - - const group = new THREE.Group() - group.rotation.set(0, -THREE.MathUtils.degToRad( - rotation * (isWall ? 90 : 45 / 2) - ), 0) - group.add(mesh) - const y = isWall ? 4.5 / 16 + mesh.scale.y / 2 : (1 - (mesh.scale.y / 2)) - group.position.set(position.x + 0.5, position.y + y, position.z + 0.5) - return group - } - - updateShowChunksBorder (value: boolean) { - this.showChunkBorders = value - for (const object of Object.values(this.sectionObjects)) { - for (const child of object.children) { - if (child.name === 'helper') { - child.visible = value - } - } - } - } - - resetWorld () { - this.active = false - for (const mesh of Object.values(this.sectionObjects)) { - this.scene.remove(mesh) - } - this.sectionObjects = {} - this.loadedChunks = {} - this.sectionsOutstanding = new Set() - for (const worker of this.workers) { - worker.postMessage({ type: 'reset' }) - } - } - - setVersion (version, texturesVersion = version) { - this.version = version - this.texturesVersion = texturesVersion - this.resetWorld() - this.active = true - - const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajor(this.version)] - for (const worker of this.workers) { - const mcData = Object.fromEntries(Object.entries(allMcData).filter(([key]) => dynamicMcDataFiles.includes(key))) - mcData.version = JSON.parse(JSON.stringify(mcData.version)) - worker.postMessage({ type: 'mcData', mcData, version: this.version }) - } - - this.updateTexturesData() - } - - updateTexturesData () { - loadTexture(this.customTexturesDataUrl || `textures/${this.texturesVersion}.png`, (texture: import('three').Texture) => { - texture.magFilter = THREE.NearestFilter - texture.minFilter = THREE.NearestFilter - texture.flipY = false - this.material.map = texture - this.material.map.onUpdate = () => { - this.downloadedTextureImage = this.material.map!.image - } - }) - - const loadBlockStates = async () => { - return new Promise(resolve => { - if (this.customBlockStatesData) return resolve(this.customBlockStatesData) - return loadJSON(`/blocksStates/${this.texturesVersion}.json`, (data) => { - this.downloadedBlockStatesData = data - // todo - this.renderUpdateEmitter.emit('blockStatesDownloaded') - resolve(data) - }) - }) - } - loadBlockStates().then((blockStates) => { - for (const worker of this.workers) { - worker.postMessage({ type: 'blockStates', json: blockStates }) - } - }) - } - - getLoadedChunksRelative (pos: Vec3, includeY = false) { - const [currentX, currentY, currentZ] = sectionPos(pos) - return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => { - const [xRaw, yRaw, zRaw] = key.split(',').map(Number) - const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw }) - const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}` - return [setKey, o] - })) - } - - addColumn (x, z, chunk) { - this.initialChunksLoad = false - this.loadedChunks[`${x},${z}`] = true - for (const worker of this.workers) { - worker.postMessage({ type: 'chunk', x, z, chunk }) - } - for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) { - const loc = new Vec3(x, y, z) - this.setSectionDirty(loc) - this.setSectionDirty(loc.offset(-16, 0, 0)) - this.setSectionDirty(loc.offset(16, 0, 0)) - this.setSectionDirty(loc.offset(0, 0, -16)) - this.setSectionDirty(loc.offset(0, 0, 16)) - } - } - - cleanChunkTextures (x, z) { - const textures = this.chunkTextures.get(`${Math.floor(x / 16)},${Math.floor(z / 16)}`) ?? {} - for (const key of Object.keys(textures)) { - textures[key].dispose() - delete textures[key] - } - } - - removeColumn (x, z) { - this.cleanChunkTextures(x, z) - - delete this.loadedChunks[`${x},${z}`] - for (const worker of this.workers) { - worker.postMessage({ type: 'unloadChunk', x, z }) - } - for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) { - this.setSectionDirty(new Vec3(x, y, z), false) - const key = `${x},${y},${z}` - const mesh = this.sectionObjects[key] - if (mesh) { - this.scene.remove(mesh) - dispose3(mesh) - } - delete this.sectionObjects[key] - } - } - - setBlockStateId (pos, stateId) { - for (const worker of this.workers) { - worker.postMessage({ type: 'blockUpdate', pos, stateId }) - } - this.setSectionDirty(pos) - if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0)) - if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0)) - if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0)) - if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0)) - if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16)) - if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16)) - } - - setSectionDirty (pos, value = true) { - this.renderUpdateEmitter.emit('dirty', pos, value) - this.cleanChunkTextures(pos.x, pos.z) // todo don't do this! - // Dispatch sections to workers based on position - // This guarantees uniformity accross workers and that a given section - // is always dispatched to the same worker - const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length) - this.workers[hash].postMessage({ type: 'dirty', x: pos.x, y: pos.y, z: pos.z, value }) - this.sectionsOutstanding.add(`${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`) - } - - // Listen for chunk rendering updates emitted if a worker finished a render and resolve if the number - // of sections not rendered are 0 - async waitForChunksToRender () { - return new Promise((resolve, reject) => { - if ([...this.sectionsOutstanding].length === 0) { - resolve() - return - } - - const updateHandler = () => { - if (this.sectionsOutstanding.size === 0) { - this.renderUpdateEmitter.removeListener('update', updateHandler) - resolve() - } - } - this.renderUpdateEmitter.on('update', updateHandler) - }) - } -} diff --git a/prismarine-viewer/viewer/lib/worldrendererCommon.ts b/prismarine-viewer/viewer/lib/worldrendererCommon.ts new file mode 100644 index 000000000..ffa764121 --- /dev/null +++ b/prismarine-viewer/viewer/lib/worldrendererCommon.ts @@ -0,0 +1,243 @@ +import * as THREE from 'three' +import { Vec3 } from 'vec3' +import { loadJSON } from './utils' +import { loadTexture } from './utils.web' +import { EventEmitter } from 'events' +import mcDataRaw from 'minecraft-data/data.js'; // handled correctly in esbuild plugin +import { dynamicMcDataFiles } from '../../buildWorkerConfig.mjs' +import { toMajor } from './version.js' +import { chunkPos } from './simpleUtils' + +function mod (x, n) { + return ((x % n) + n) % n +} + +export abstract class WorldRendererCommon { + worldConfig = { minY: 0, worldHeight: 256 } + // todo @sa2urami set alphaTest back to 0.1 and instead properly sort transparent and solid objects (needs to be done in worker too) + material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.5 }) + + showChunkBorders = false + active = false + version = undefined as string | undefined + loadedChunks = {} as Record + finishedChunks = {} as Record + sectionsOutstanding = new Map() + renderUpdateEmitter = new EventEmitter() + customBlockStatesData = undefined as any + customTexturesDataUrl = undefined as string | undefined + downloadedBlockStatesData = undefined as any + downloadedTextureImage = undefined as any + workers: any[] = [] + viewerPosition?: Vec3 + lastCamUpdate = 0 + droppedFpsPercentage = 0 + initialChunksLoad = true + enableChunksLoadDelay = false + texturesVersion?: string + viewDistance = -1 + chunksLength = 0 + // promisesQueue = [] as Promise[] + + abstract outputFormat: 'threeJs' | 'webgl' + + constructor (numWorkers: number) { + // init workers + for (let i = 0; i < numWorkers; i++) { + // Node environment needs an absolute path, but browser needs the url of the file + let src = __dirname + if (typeof window === 'undefined') src += '/worker.js' + else src = 'worker.js' + + const worker: any = new Worker(src) + worker.onmessage = async ({ data }) => { + if (!this.active) return + this.handleWorkerMessage(data) + await new Promise(resolve => { + setTimeout(resolve, 0) + }) + if (data.type === 'sectionFinished') { + if (!this.sectionsOutstanding.get(data.key)) throw new Error(`sectionFinished event for non-outstanding section ${data.key}`) + this.sectionsOutstanding.set(data.key, this.sectionsOutstanding.get(data.key)! - 1) + if (this.sectionsOutstanding.get(data.key) === 0) this.sectionsOutstanding.delete(data.key) + + const chunkCoords = data.key.split(',').map(Number) + if (this.loadedChunks[`${chunkCoords[0]},${chunkCoords[2]}`]) { // ensure chunk data was added, not a neighbor chunk update + const loadingKeys = [...this.sectionsOutstanding.keys()] + if (!loadingKeys.some(key => { + const [x, y, z] = key.split(',').map(Number) + return x === chunkCoords[0] && z === chunkCoords[2] + })) { + this.finishedChunks[`${chunkCoords[0]},${chunkCoords[2]}`] = true + } + } + if (this.sectionsOutstanding.size === 0) { + const allFinished = Object.keys(this.finishedChunks).length === this.chunksLength + if (allFinished) { + this.allChunksLoaded?.() + } + } + + this.renderUpdateEmitter.emit('update') + } + } + if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) }) + this.workers.push(worker) + } + } + + abstract handleWorkerMessage (data: WorkerReceive): void + + /** + * Optionally update data that are depedendent on the viewer position + */ + updatePosDataChunk?(key: string): void + + allChunksLoaded?(): void + + updateViewerPosition (pos: Vec3) { + this.viewerPosition = pos + for (const [key, value] of Object.entries(this.loadedChunks)) { + if (!value) continue + this.updatePosDataChunk?.(key) + } + } + + sendWorkers (message: WorkerSend) { + for (const worker of this.workers) { + worker.postMessage(message) + } + } + + getDistance (posAbsolute: Vec3) { + const [botX, botZ] = chunkPos(this.viewerPosition!) + const dx = Math.abs(botX - Math.floor(posAbsolute.x / 16)) + const dz = Math.abs(botZ - Math.floor(posAbsolute.z / 16)) + return [dx, dz] as [number, number] + } + + abstract updateShowChunksBorder (value: boolean): void + + resetWorld () { + this.active = false + this.loadedChunks = {} + this.sectionsOutstanding = new Map() + for (const worker of this.workers) { + worker.postMessage({ type: 'reset' }) + } + } + + setVersion (version, texturesVersion = version) { + this.version = version + this.texturesVersion = texturesVersion + this.resetWorld() + this.active = true + + const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajor(this.version)] + for (const worker of this.workers) { + const mcData = Object.fromEntries(Object.entries(allMcData).filter(([key]) => dynamicMcDataFiles.includes(key))) + mcData.version = JSON.parse(JSON.stringify(mcData.version)) + worker.postMessage({ type: 'mcData', mcData, version: this.version }) + } + + this.updateTexturesData() + } + + updateTexturesData () { + loadTexture(this.customTexturesDataUrl || `textures/${this.texturesVersion}.png`, (texture: import('three').Texture) => { + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + texture.flipY = false + this.material.map = texture + }, (tex) => { + this.downloadedTextureImage = this.material.map!.image + const loadBlockStates = async () => { + return new Promise(resolve => { + if (this.customBlockStatesData) return resolve(this.customBlockStatesData) + return loadJSON(`/blocksStates/${this.texturesVersion}.json`, (data) => { + this.downloadedBlockStatesData = data + // todo + this.renderUpdateEmitter.emit('blockStatesDownloaded') + resolve(data) + }) + }) + } + loadBlockStates().then((blockStates) => { + for (const worker of this.workers) { + worker.postMessage({ type: 'rendererData', json: blockStates, textureSize: tex.image.width, outputFormat: this.outputFormat }) + } + }) + }) + + } + + addColumn (x, z, chunk) { + this.initialChunksLoad = false + this.loadedChunks[`${x},${z}`] = true + for (const worker of this.workers) { + worker.postMessage({ type: 'chunk', x, z, chunk }) + } + for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) { + const loc = new Vec3(x, y, z) + this.setSectionDirty(loc) + this.setSectionDirty(loc.offset(-16, 0, 0)) + this.setSectionDirty(loc.offset(16, 0, 0)) + this.setSectionDirty(loc.offset(0, 0, -16)) + this.setSectionDirty(loc.offset(0, 0, 16)) + } + } + + removeColumn (x, z) { + delete this.loadedChunks[`${x},${z}`] + for (const worker of this.workers) { + worker.postMessage({ type: 'unloadChunk', x, z }) + } + } + + setBlockStateId (pos: Vec3, stateId: number) { + for (const worker of this.workers) { + worker.postMessage({ type: 'blockUpdate', pos, stateId }) + } + this.setSectionDirty(pos) + if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0)) + if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0)) + if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0)) + if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0)) + if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16)) + if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16)) + } + + setSectionDirty (pos: Vec3, value = true) { + if (this.viewDistance === -1) throw new Error('viewDistance not set') + const distance = this.getDistance(pos) + if (distance[0] > this.viewDistance || distance[1] > this.viewDistance) return + const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`; + // if (this.sectionsOutstanding.has(key)) return + this.renderUpdateEmitter.emit('dirty', pos, value) + // Dispatch sections to workers based on position + // This guarantees uniformity accross workers and that a given section + // is always dispatched to the same worker + const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length) + this.sectionsOutstanding.set(key, (this.sectionsOutstanding.get(key) ?? 0) + 1) + this.workers[hash].postMessage({ type: 'dirty', x: pos.x, y: pos.y, z: pos.z, value }) + } + + // Listen for chunk rendering updates emitted if a worker finished a render and resolve if the number + // of sections not rendered are 0 + async waitForChunksToRender () { + return new Promise((resolve, reject) => { + if ([...this.sectionsOutstanding].length === 0) { + resolve() + return + } + + const updateHandler = () => { + if (this.sectionsOutstanding.size === 0) { + this.renderUpdateEmitter.removeListener('update', updateHandler) + resolve() + } + } + this.renderUpdateEmitter.on('update', updateHandler) + }) + } +} diff --git a/prismarine-viewer/viewer/lib/worldrendererThree.ts b/prismarine-viewer/viewer/lib/worldrendererThree.ts new file mode 100644 index 000000000..5c17153f8 --- /dev/null +++ b/prismarine-viewer/viewer/lib/worldrendererThree.ts @@ -0,0 +1,215 @@ +import * as THREE from 'three' +import { Vec3 } from 'vec3' +import nbt from 'prismarine-nbt' +import { dispose3 } from './dispose' +import PrismarineChatLoader from 'prismarine-chat' +import { renderSign } from '../sign-renderer/' +import { chunkPos, sectionPos } from './simpleUtils' +import { WorldRendererCommon } from './worldrendererCommon' + +function mod (x, n) { + return ((x % n) + n) % n +} + +export class WorldRendererThree extends WorldRendererCommon { + outputFormat = 'threeJs' as const + blockEntities = {} + sectionObjects: Record = {} + showChunkBorders = false + chunkTextures = new Map() + signsCache = new Map() + + constructor(public scene: THREE.Scene, numWorkers = 4) { + super(numWorkers) + } + + /** + * Optionally update data that are depedendent on the viewer position + */ + updatePosDataChunk (key: string) { + if (!this.viewerPosition) return + const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16)) + const [xPlayer, yPlayer, zPlayer] = this.viewerPosition.toArray().map(x => Math.floor(x / 16)) + // sum of distances: x + y + z + const chunkDistance = Math.abs(x - xPlayer) + Math.abs(y - yPlayer) + Math.abs(z - zPlayer) + const section = this.sectionObjects[key].children.find(child => child.name === 'mesh')! + section.renderOrder = 500 - chunkDistance + } + + handleWorkerMessage (data: any): void { + if (data.type !== 'geometry') return + let object: THREE.Object3D = this.sectionObjects[data.key] + if (object) { + this.scene.remove(object) + dispose3(object) + delete this.sectionObjects[data.key] + } + + const chunkCoords = data.key.split(',') + if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return + + // if (!this.initialChunksLoad && this.enableChunksLoadDelay) { + // const newPromise = new Promise(resolve => { + // if (this.droppedFpsPercentage > 0.5) { + // setTimeout(resolve, 1000 / 50 * this.droppedFpsPercentage) + // } else { + // setTimeout(resolve) + // } + // }) + // this.promisesQueue.push(newPromise) + // for (const promise of this.promisesQueue) { + // await promise + // } + // } + + const geometry = new THREE.BufferGeometry() + geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3)) + geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3)) + geometry.setAttribute('color', new THREE.BufferAttribute(data.geometry.colors, 3)) + geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2)) + geometry.setIndex(data.geometry.indices) + + const mesh = new THREE.Mesh(geometry, this.material) + mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz) + mesh.name = 'mesh' + object = new THREE.Group() + object.add(mesh) + const boxHelper = new THREE.BoxHelper(mesh, 0xffff00) + boxHelper.name = 'helper' + object.add(boxHelper) + object.name = 'chunk' + if (!this.showChunkBorders) { + boxHelper.visible = false + } + // should not compute it once + if (Object.keys(data.geometry.signs).length) { + for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.signs)) { + const [x, y, z] = posKey.split(',') + const signBlockEntity = this.blockEntities[posKey] + if (!signBlockEntity) continue + const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(signBlockEntity)); + if (!sign) continue + object.add(sign) + } + } + this.sectionObjects[data.key] = object + this.updatePosDataChunk(data.key) + this.scene.add(object) + } + + getSignTexture (position: Vec3, blockEntity, backSide = false) { + const chunk = chunkPos(position) + let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`) + if (!textures) { + textures = {} + this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures) + } + const texturekey = `${position.x},${position.y},${position.z}`; + // todo investigate bug and remove this so don't need to clean in section dirty + if (textures[texturekey]) return textures[texturekey] + + const PrismarineChat = PrismarineChatLoader(this.version!) + const canvas = renderSign(blockEntity, PrismarineChat) + if (!canvas) return + const tex = new THREE.Texture(canvas) + tex.magFilter = THREE.NearestFilter + tex.minFilter = THREE.NearestFilter + tex.needsUpdate = true + textures[texturekey] = tex + return tex + } + + renderSign (position: Vec3, rotation: number, isWall: boolean, blockEntity) { + const tex = this.getSignTexture(position, blockEntity) + + if (!tex) return + + // todo implement + // const key = JSON.stringify({ position, rotation, isWall }) + // if (this.signsCache.has(key)) { + // console.log('cached', key) + // } else { + // this.signsCache.set(key, tex) + // } + + const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true, })) + mesh.renderOrder = 999 + + // todo @sa2urami shouldnt all this be done in worker? + mesh.scale.set(1, 7 / 16, 1) + if (isWall) { + mesh.position.set(0, 0, -(8 - 1.5) / 16 + 0.001) + } else { + // standing + const faceEnd = 8.75 + mesh.position.set(0, 0, (faceEnd - 16 / 2) / 16 + 0.001) + } + + const group = new THREE.Group() + group.rotation.set(0, -THREE.MathUtils.degToRad( + rotation * (isWall ? 90 : 45 / 2) + ), 0) + group.add(mesh) + const y = isWall ? 4.5 / 16 + mesh.scale.y / 2 : (1 - (mesh.scale.y / 2)) + group.position.set(position.x + 0.5, position.y + y, position.z + 0.5) + return group + } + + updateShowChunksBorder (value: boolean) { + this.showChunkBorders = value + for (const object of Object.values(this.sectionObjects)) { + for (const child of object.children) { + if (child.name === 'helper') { + child.visible = value + } + } + } + } + + resetWorld () { + super.resetWorld() + + for (const mesh of Object.values(this.sectionObjects)) { + this.scene.remove(mesh) + } + } + + getLoadedChunksRelative (pos: Vec3, includeY = false) { + const [currentX, currentY, currentZ] = sectionPos(pos) + return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => { + const [xRaw, yRaw, zRaw] = key.split(',').map(Number) + const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw }) + const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}` + return [setKey, o] + })) + } + + cleanChunkTextures (x, z) { + const textures = this.chunkTextures.get(`${Math.floor(x / 16)},${Math.floor(z / 16)}`) ?? {} + for (const key of Object.keys(textures)) { + textures[key].dispose() + delete textures[key] + } + } + + removeColumn (x, z) { + super.removeColumn(x, z) + + this.cleanChunkTextures(x, z) + for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) { + this.setSectionDirty(new Vec3(x, y, z), false) + const key = `${x},${y},${z}` + const mesh = this.sectionObjects[key] + if (mesh) { + this.scene.remove(mesh) + dispose3(mesh) + } + delete this.sectionObjects[key] + } + } + + setSectionDirty (pos, value = true) { + this.cleanChunkTextures(pos.x, pos.z) // todo don't do this! + super.setSectionDirty(pos, value) + } +} diff --git a/prismarine-viewer/viewer/lib/worldrendererWebgl.ts b/prismarine-viewer/viewer/lib/worldrendererWebgl.ts new file mode 100644 index 000000000..a8dcfff06 --- /dev/null +++ b/prismarine-viewer/viewer/lib/worldrendererWebgl.ts @@ -0,0 +1,106 @@ +import { Vec3 } from 'vec3' +import { updateStatText } from '../../examples/newStats' +import { addBlocksSection, removeBlocksSection, sendWorkerMessage } from '../../examples/webglRenderer' +import type { WebglData } from '../prepare/webglData' +import { loadJSON } from './utils.web' +import { WorldRendererCommon } from './worldrendererCommon' + +export class WorldRendererWebgl extends WorldRendererCommon { + outputFormat = 'webgl' as const + newChunks = {} as Record + webglData: WebglData + stopBlockUpdate = false + lastChunkDistance = 0 + + constructor(numWorkers = 4) { + super(numWorkers) + + this.renderUpdateEmitter.on('update', () => { + const loadedChunks = Object.keys(this.finishedChunks).length; + updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance})`) + }) + } + + playgroundGetWebglData () { + const playgroundChunk = Object.values(this.newChunks).filter((x: any) => Object.keys(x?.blocks ?? {}).length > 0)?.[0] as any + if (!playgroundChunk) return + const block = Object.values(playgroundChunk.blocks)?.[0] as any + if (!block) return + const { textureName } = block + if (!textureName) return + return this.webglData[textureName] + } + + setBlockStateId (pos: any, stateId: any): void { + if (this.stopBlockUpdate) return + super.setBlockStateId(pos, stateId) + } + + isWaitingForChunksToRender = false + + allChunksLoaded (): void { + console.log('allChunksLoaded') + sendWorkerMessage({ + type: 'addBlocksSectionDone' + }) + } + + handleWorkerMessage (data: any): void { + if (data.type === 'geometry' && Object.keys(data.geometry.blocks).length) { + + const chunkCoords = data.key.split(',').map(Number) as [number, number, number] + if (/* !this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || */ !this.active) return + + addBlocksSection(data.key, data.geometry) + this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2]))) + + // todo + this.newChunks[data.key] = data.geometry + } + } + + chunksReset () { + sendWorkerMessage({ + type: 'fullReset' + }) + } + + updatePosDataChunk (key: string) { + } + + updateTexturesData (): void { + super.updateTexturesData() + loadJSON(`/webgl/${this.texturesVersion}.json`, (json) => { + this.webglData = json + }) + } + + updateShowChunksBorder (value: boolean) { + // todo + } + + + removeColumn (x, z) { + console.log('removeColumn', x, z) + super.removeColumn(x, z) + for (const key of Object.keys(this.newChunks)) { + const [xSec, _ySec, zSec] = key.split(',').map(Number) + // if (Math.floor(x / 16) === x && Math.floor(z / 16) === z) { + if (x === xSec && z === zSec) { + // foundSections.push(key) + removeBlocksSection(key) + } + } + // for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) { + // this.setSectionDirty(new Vec3(x, y, z), false) + // const key = `${x},${y},${z}` + // const mesh = this.sectionObjects[key] + // if (mesh) { + // this.scene.remove(mesh) + // dispose3(mesh) + // } + // delete this.sectionObjects[key] + // } + } + +} diff --git a/prismarine-viewer/viewer/prepare/atlas.ts b/prismarine-viewer/viewer/prepare/atlas.ts index cb30b7276..8da53e0c3 100644 --- a/prismarine-viewer/viewer/prepare/atlas.ts +++ b/prismarine-viewer/viewer/prepare/atlas.ts @@ -31,11 +31,20 @@ export type JsonAtlas = { [file: string]: { u: number, v: number, + su?: number, + sv?: number, + animatedFrames?: number } } } -export const makeTextureAtlas = (input: string[], getInputData: (name) => { contents: string, tileWidthMult?: number }, tilesCount = input.length, suSvOptimize: 'remove' | null = null): { +export const makeTextureAtlas = ( + input: string[], + getInputData: (name) => { contents: string, tileWidthMult?: number }, + tilesCount = input.length, + suSvOptimize: 'remove' | null = null, + renderAnimated = true +): { image: Buffer, canvas: Canvas, json: JsonAtlas @@ -47,7 +56,7 @@ export const makeTextureAtlas = (input: string[], getInputData: (name) => { cont const canvas = new Canvas(imgSize, imgSize, 'png' as any) const g = canvas.getContext('2d') - const texturesIndex = {} + const texturesIndex = {} as JsonAtlas['textures'] let offset = 0 const suSv = tileSize / imgSize @@ -61,7 +70,28 @@ export const makeTextureAtlas = (input: string[], getInputData: (name) => { cont const inputData = getInputData(keyValue); img.src = inputData.contents const renderWidth = tileSize * (inputData.tileWidthMult ?? 1) - g.drawImage(img, 0, 0, renderWidth, tileSize, x, y, renderWidth, tileSize) + let animatedFrames = 0 + const addDebugText = (x, y) => { + return // disable debug text + g.fillStyle = 'black' + g.font = '8px Arial' + g.fillText(i, x, y) + } + if (img.height > tileSize && renderAnimated) { + const frames = img.height / tileSize; + animatedFrames = frames + console.log("Animated texture", keyValue, frames) + offset += frames - 1 + for (let i = 0; i < frames; i++) { + const x = ((pos + i) % texSize) * tileSize + const y = Math.floor((pos + i) / texSize) * tileSize + g.drawImage(img, 0, i * tileSize, renderWidth, tileSize, x, y, renderWidth, tileSize) + addDebugText(x, y) + } + } else { + g.drawImage(img, 0, 0, renderWidth, tileSize, x, y, renderWidth, tileSize) + addDebugText(x, y) + } const cleanName = keyValue.split('.').slice(0, -1).join('.') || keyValue texturesIndex[cleanName] = { @@ -70,7 +100,9 @@ export const makeTextureAtlas = (input: string[], getInputData: (name) => { cont ...suSvOptimize === 'remove' ? {} : { su: suSv, sv: suSv - } + }, + textureName: cleanName, + animatedFrames: animatedFrames || undefined } } diff --git a/prismarine-viewer/viewer/prepare/generateTextures.ts b/prismarine-viewer/viewer/prepare/generateTextures.ts index b9a5e3e5f..2c0e3902d 100644 --- a/prismarine-viewer/viewer/prepare/generateTextures.ts +++ b/prismarine-viewer/viewer/prepare/generateTextures.ts @@ -5,11 +5,12 @@ import mcAssets from 'minecraft-assets' import fs from 'fs-extra' import { prepareMoreGeneratedBlocks } from './moreGeneratedBlocks' import { generateItemsAtlases } from './genItemsAtlas' +import { prepareWebglData } from './webglData' const publicPath = path.resolve(__dirname, '../../public') const texturesPath = path.join(publicPath, 'textures') -if (fs.existsSync(texturesPath) && !process.argv.includes('-f')) { +if (fs.existsSync(texturesPath) && !process.argv.includes('-f') && !process.argv.includes('-l')) { console.log('textures folder already exists, skipping...') process.exit(0) } @@ -17,14 +18,16 @@ fs.mkdirSync(texturesPath, { recursive: true }) const blockStatesPath = path.join(publicPath, 'blocksStates') fs.mkdirSync(blockStatesPath, { recursive: true }) +fs.mkdirSync(path.join(publicPath, 'webgl'), { recursive: true }) const warnings = new Set() Promise.resolve().then(async () => { generateItemsAtlases() console.time('generateTextures') - for (const version of mcAssets.versions as typeof mcAssets['versions']) { + const versions = process.argv.includes('-l') ? [mcAssets.versions.at(-1)!] : mcAssets.versions + for (const version of versions as typeof mcAssets['versions']) { // for debugging (e.g. when above is overridden) - if (!mcAssets.versions.includes(version)) { + if (!versions.includes(version)) { throw new Error(`Version ${version} is not supported by minecraft-assets`) } const assets = mcAssets(version) @@ -40,11 +43,13 @@ Promise.resolve().then(async () => { const blocksStates = JSON.stringify(prepareBlocksStates(assets, atlas)) fs.writeFileSync(path.resolve(blockStatesPath, version + '.json'), blocksStates) + const webglData = prepareWebglData(path.join(assets.directory, 'blocks'), atlas.json) + fs.writeFileSync(path.resolve(publicPath, 'webgl', version + '.json'), JSON.stringify(webglData)) fs.copySync(assets.directory, path.resolve(texturesPath, version), { overwrite: true }) } - fs.writeFileSync(path.join(publicPath, 'supportedVersions.json'), '[' + mcAssets.versions.map(v => `"${v}"`).toString() + ']') + fs.writeFileSync(path.join(publicPath, 'supportedVersions.json'), '[' + versions.map(v => `"${v}"`).toString() + ']') warnings.forEach(x => console.warn(x)) console.timeEnd('generateTextures') }) diff --git a/prismarine-viewer/viewer/prepare/webglData.ts b/prismarine-viewer/viewer/prepare/webglData.ts new file mode 100644 index 000000000..937637e36 --- /dev/null +++ b/prismarine-viewer/viewer/prepare/webglData.ts @@ -0,0 +1,28 @@ +import { JsonAtlas } from './atlas'; +import { join } from 'path' +import fs from 'fs' + +export type WebglData = ReturnType + +export const prepareWebglData = (blockTexturesDir: string, atlas: JsonAtlas) => { + // todo + return Object.fromEntries(Object.entries(atlas.textures).map(([texture, { animatedFrames }]) => { + if (!animatedFrames) return null! + const mcMeta = JSON.parse(fs.readFileSync(join(blockTexturesDir, texture + '.png.mcmeta'), 'utf8')) as { + animation: { + interpolate: boolean, + frametime: number, + frames: ({ + index: number, + time: number + } | number)[] + } + } + return [texture, { + animation: { + ...mcMeta.animation, + framesCount: animatedFrames + } + }] as const + }).filter(Boolean)) +} diff --git a/scripts/build.js b/scripts/build.js index 9840e5d3e..61c09b20c 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -13,6 +13,7 @@ const entityMcAssets = McAssets('1.16.4') // these files could be copied at build time eg with copy plugin, but copy plugin slows down the config so we copy them there, alternative we could inline it in esbuild config const filesToCopy = [ { from: `${prismarineViewerBase}/public/blocksStates/`, to: 'dist/blocksStates/' }, + { from: `${prismarineViewerBase}/public/webgl/`, to: 'dist/webgl/' }, { from: `${prismarineViewerBase}/public/worker.js`, to: 'dist/worker.js' }, { from: './assets/', to: './dist/' }, { from: './config.json', to: 'dist/config.json' }, @@ -92,7 +93,7 @@ exports.getSwAdditionalEntries = () => { } exports.moveStorybookFiles = () => { - fsExtra.moveSync('storybook-static', 'dist/storybook', {overwrite: true,}) + fsExtra.moveSync('storybook-static', 'dist/storybook', { overwrite: true, }) fsExtra.copySync('dist/storybook', '.vercel/output/static/storybook') } diff --git a/scripts/esbuildPlugins.mjs b/scripts/esbuildPlugins.mjs index 95d88b8ed..4f9b0ddd4 100644 --- a/scripts/esbuildPlugins.mjs +++ b/scripts/esbuildPlugins.mjs @@ -41,6 +41,11 @@ const plugins = [ return { contents: `window.mcData ??= ${JSON.stringify(defaultVersionsObj)};module.exports = { pc: window.mcData }`, loader: 'js', + watchFiles: [ + // todo + 'dist/worker.js', + 'dist/webglRendererWorker.js' + ], } }) build.onResolve({ @@ -148,8 +153,9 @@ const plugins = [ //@ts-ignore const outputFile = outputFiles.find(x => x.path.endsWith('.js')) if (outputFile.hash === prevHash) { - console.log('Ignoring reload as contents the same') - return + // todo also check workers and css + // console.log('Ignoring reload as contents the same') + // return } prevHash = outputFile.hash let outputText = outputFile.text diff --git a/scripts/githubActions.mjs b/scripts/githubActions.mjs index ba7b8566d..ab786ea94 100644 --- a/scripts/githubActions.mjs +++ b/scripts/githubActions.mjs @@ -6,10 +6,10 @@ const fns = { async getAlias () { const aliasesRaw = process.env.ALIASES if (!aliasesRaw) throw new Error('No aliases found') - const aliases = aliasesRaw.split('\n').map((x) => x.split('=')) + const aliases = aliasesRaw.split('\n').map((x) => x.trim().split('=')) const githubActionsPull = process.env.PULL_URL?.split('/').at(-1) - if (!githubActionsPull) throw new Error(`Not a pull request, got ${process.env.GITHUB_REF}`) - const prNumber = githubActionsPull[1] + if (!githubActionsPull) throw new Error(`Not a pull request, got ${process.env.PULL_URL}`) + const prNumber = githubActionsPull const alias = aliases.find((x) => x[0] === prNumber) if (alias) { // set github output @@ -18,7 +18,7 @@ const fns = { } } -function setOutput(key, value) { +function setOutput (key, value) { // Temporary hack until core actions library catches up with github new recommendations const output = process.env['GITHUB_OUTPUT'] fs.appendFileSync(output, `${key}=${value}${os.EOL}`) diff --git a/src/controls.ts b/src/controls.ts index 48c051791..cd0afad6e 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -288,6 +288,10 @@ export const f3Keybinds = [ console.warn('forcefully removed chunk from scene') } } + + viewer.world.chunksReset() // todo + viewer.world.newChunks = {} + if (localServer) { //@ts-expect-error not sure why it is private... maybe revisit api? localServer.players[0].world.columns = {} diff --git a/src/globals.d.ts b/src/globals.d.ts index 75c9d0a0f..c2c5ac6b3 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -72,6 +72,14 @@ declare module '*.png' { const png: string export default png } +declare module '*.frag' { + const png: string + export default png +} +declare module '*.vert' { + const png: string + export default png +} interface PromiseConstructor { withResolvers (): { diff --git a/src/index.ts b/src/index.ts index 3a8fbd5cf..8310b810b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -96,6 +96,9 @@ import { handleMovementStickDelta, joystickPointer } from './react/TouchAreasCon import { possiblyHandleStateVariable } from './googledrive' import flyingSquidEvents from './flyingSquidEvents' import { hideNotification, notificationProxy } from './react/NotificationProvider' +import { initWebglRenderer } from 'prismarine-viewer/examples/webglRenderer' +import { addNewStat } from 'prismarine-viewer/examples/newStats' +// import { ViewerBase } from 'prismarine-viewer/viewer/lib/viewerWrapper' window.debug = debug window.THREE = THREE @@ -122,12 +125,6 @@ try { // renderer.localClippingEnabled = true initWithRenderer(renderer.domElement) window.renderer = renderer -let pixelRatio = window.devicePixelRatio || 1 // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable -if (!renderer.capabilities.isWebGL2) pixelRatio = 1 // webgl1 has issues with high pixel ratio (sometimes screen is clipped) -renderer.setPixelRatio(pixelRatio) -renderer.setSize(window.innerWidth, window.innerHeight) -renderer.domElement.id = 'viewer-canvas' -document.body.appendChild(renderer.domElement) const isFirefox = ua.getBrowser().name === 'Firefox' if (isFirefox) { @@ -172,62 +169,6 @@ watchValue(options, (o) => { }) let postRenderFrameFn = () => { } -let delta = 0 -let lastTime = performance.now() -let previousWindowWidth = window.innerWidth -let previousWindowHeight = window.innerHeight -let max = 0 -let rendered = 0 -const renderFrame = (time: DOMHighResTimeStamp) => { - if (window.stopLoop) return - for (const fn of beforeRenderFrame) fn() - window.requestAnimationFrame(renderFrame) - if (window.stopRender || renderer.xr.isPresenting) return - if (renderInterval) { - delta += time - lastTime - lastTime = time - if (delta > renderInterval) { - delta %= renderInterval - // continue rendering - } else { - return - } - } - // ios bug: viewport dimensions are updated after the resize event - if (previousWindowWidth !== window.innerWidth || previousWindowHeight !== window.innerHeight) { - resizeHandler() - previousWindowWidth = window.innerWidth - previousWindowHeight = window.innerHeight - } - statsStart() - viewer.update() - viewer.render() - rendered++ - postRenderFrameFn() - statsEnd() -} -renderFrame(performance.now()) -setInterval(() => { - if (max > 0) { - viewer.world.droppedFpsPercentage = rendered / max - } - max = Math.max(rendered, max) - rendered = 0 -}, 1000) - -const resizeHandler = () => { - const width = window.innerWidth - const height = window.innerHeight - - viewer.camera.aspect = width / height - viewer.camera.updateProjectionMatrix() - renderer.setSize(width, height) - - if (viewer.composer) { - viewer.updateComposerSize() - } -} - const hud = document.getElementById('hud') const pauseMenu = document.getElementById('pause-screen') @@ -442,10 +383,16 @@ async function connect (connectOptions: { viewer.setVersion(version) } + serverOptions.version = '1.14.4' const downloadVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined) if (downloadVersion) { await downloadMcData(downloadVersion) } + await initWebglRenderer(downloadVersion, () => { + postRenderFrameFn() + viewer.update() + }) + addNewStat('loaded-chunks') if (singleplayer) { // SINGLEPLAYER EXPLAINER: @@ -919,6 +866,7 @@ downloadAndOpenFile().then((downloadAction) => { const initialLoader = document.querySelector('.initial-loader') as HTMLElement | null if (initialLoader) { initialLoader.style.opacity = '0' + initialLoader.style.pointerEvents = 'none' } window.pageLoaded = true diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 8490053cb..de987378c 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -61,7 +61,9 @@ const defaultOptions = { excludeCommunicationDebugEvents: [], preventDevReloadWhilePlaying: false, numWorkers: 4, - localServerOptions: {} as any, + localServerOptions: { + gameMode: 1 + } as any, preferLoadReadonly: false, disableLoadPrompts: false, guestUsername: 'guest', diff --git a/src/playerWindows.ts b/src/playerWindows.ts index b94d0f028..a4a6cb13f 100644 --- a/src/playerWindows.ts +++ b/src/playerWindows.ts @@ -127,7 +127,7 @@ export const onGameLoad = (onLoad) => { }) } -const findTextureInBlockStates = (name) => { +export const findTextureInBlockStates = (name) => { assertDefined(viewer) const blockStates: BlockStates = viewer.world.customBlockStatesData || viewer.world.downloadedBlockStatesData const vars = blockStates[name]?.variants diff --git a/src/react/CreateWorld.tsx b/src/react/CreateWorld.tsx index ceed77ec5..87b367774 100644 --- a/src/react/CreateWorld.tsx +++ b/src/react/CreateWorld.tsx @@ -8,17 +8,19 @@ import styles from './createWorld.module.css' // const worldTypes = ['default', 'flat', 'largeBiomes', 'amplified', 'customized', 'buffet', 'debug_all_block_states'] const worldTypes = ['default', 'flat'/* , 'void' */] +const gameModes = ['survival', 'creative'/* , 'adventure', 'spectator' */] export const creatingWorldState = proxy({ title: '', type: worldTypes[0], + gameMode: gameModes[0], version: '' }) export default ({ cancelClick, createClick, customizeClick, versions, defaultVersion }) => { const [quota, setQuota] = useState('') - const { title, type, version } = useSnapshot(creatingWorldState) + const { title, type, version, gameMode } = useSnapshot(creatingWorldState) useEffect(() => { creatingWorldState.version = defaultVersion void navigator.storage?.estimate?.().then(({ quota, usage }) => { @@ -54,9 +56,15 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer - + {/* */} +
Default and other world types are WIP
diff --git a/src/react/CreateWorldProvider.tsx b/src/react/CreateWorldProvider.tsx index 58208f955..332a1d31c 100644 --- a/src/react/CreateWorldProvider.tsx +++ b/src/react/CreateWorldProvider.tsx @@ -23,7 +23,7 @@ export default () => { }} createClick={async () => { // create new world - const { title, type, version } = creatingWorldState + const { title, type, version, gameMode } = creatingWorldState // todo display path in ui + disable if exist const savePath = await uniqueFileNameFromWorldName(title, getWorldsPath()) await mkdirRecursive(savePath) @@ -51,7 +51,8 @@ export default () => { levelName: title, version, generation, - 'worldFolder': savePath + 'worldFolder': savePath, + gameMode: gameMode === 'survival' ? 0 : 1, }, })) }} diff --git a/src/topRightStats.ts b/src/topRightStats.ts index a508a2c24..ac3898e52 100644 --- a/src/topRightStats.ts +++ b/src/topRightStats.ts @@ -39,7 +39,7 @@ if (hasRamPanel) { addStat(stats2.dom) } -const hideStats = localStorage.hideStats || isCypress() +const hideStats = localStorage.hideStats || isCypress() || true if (hideStats) { stats.dom.style.display = 'none' stats2.dom.style.display = 'none' diff --git a/src/utils.ts b/src/utils.ts index 39bf940e1..fc73514c7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -161,11 +161,19 @@ let prevRenderDistance = options.renderDistance export const setRenderDistance = () => { assertDefined(worldView) const { renderDistance: singleplayerRenderDistance, multiplayerRenderDistance } = options - const renderDistance = miscUiState.singleplayer ? singleplayerRenderDistance : multiplayerRenderDistance + let renderDistance = miscUiState.singleplayer ? singleplayerRenderDistance : multiplayerRenderDistance + const zeroRenderDistance = miscUiState.singleplayer && renderDistance === 0 + if (zeroRenderDistance) { + renderDistance = 1 // mineflayer limitation workaround + } bot.setSettings({ viewDistance: renderDistance }) - worldView.viewDistance = renderDistance + if (zeroRenderDistance) { + localServer!.players[0].view = 0 + renderDistance = 0 + } + worldView.updateViewDistance(renderDistance) prevRenderDistance = renderDistance } export const reloadChunks = async () => { diff --git a/src/workerWorkaround.ts b/src/workerWorkaround.ts new file mode 100644 index 000000000..00419b8c3 --- /dev/null +++ b/src/workerWorkaround.ts @@ -0,0 +1,2 @@ +global = globalThis +globalThis.window = globalThis diff --git a/src/worldSaveWorker.ts b/src/worldSaveWorker.ts new file mode 100644 index 000000000..b49c02003 --- /dev/null +++ b/src/worldSaveWorker.ts @@ -0,0 +1,2 @@ +import './workerWorkaround' +import './browserfs'